u
This commit is contained in:
@@ -12,7 +12,7 @@
|
|||||||
body,
|
body,
|
||||||
#root {
|
#root {
|
||||||
min-height: 100%;
|
min-height: 100%;
|
||||||
background-color: #dceeea;
|
background-color: #c5ddd8;
|
||||||
}
|
}
|
||||||
|
|
||||||
html,
|
html,
|
||||||
|
|||||||
@@ -1,2 +1,2 @@
|
|||||||
// Mirrored in index.html and index.css for paint before JS loads.
|
// Mirrored in index.html and index.css for paint before JS loads.
|
||||||
export const MAIN_BACKGROUND = '#dceeea';
|
export const MAIN_BACKGROUND = '#c5ddd8';
|
||||||
|
|||||||
@@ -13,6 +13,7 @@ import {
|
|||||||
primaryPrice,
|
primaryPrice,
|
||||||
} from '../../utils/articleDetail';
|
} from '../../utils/articleDetail';
|
||||||
import { formatPrice } from '../../utils/price';
|
import { formatPrice } from '../../utils/price';
|
||||||
|
import { PANEL_TILE_KEYS } from '../../theme';
|
||||||
import { formatStock } from '../../pages/artikel/searchFormat';
|
import { formatStock } from '../../pages/artikel/searchFormat';
|
||||||
|
|
||||||
function PanelValue({ label, children }) {
|
function PanelValue({ label, children }) {
|
||||||
@@ -46,7 +47,6 @@ function ArticlePhoto() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const headerTileSx = {
|
const headerTileSx = {
|
||||||
p: 1.25,
|
|
||||||
minHeight: 0,
|
minHeight: 0,
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -99,6 +99,7 @@ export default function ArticleView({ article: articleData, onArticleUpdated })
|
|||||||
<PanelTileGrid>
|
<PanelTileGrid>
|
||||||
<EditablePanelTile
|
<EditablePanelTile
|
||||||
wide={3}
|
wide={3}
|
||||||
|
tileKey={PANEL_TILE_KEYS.name}
|
||||||
minHeight={0}
|
minHeight={0}
|
||||||
sx={headerTileSx}
|
sx={headerTileSx}
|
||||||
value={{ name: articleData.name ?? '' }}
|
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 />
|
<ArticlePhoto />
|
||||||
</PanelTile>
|
</PanelTile>
|
||||||
|
|
||||||
<PanelTile wide={1} title="Bestand">
|
<PanelTile wide={1} tileKey={PANEL_TILE_KEYS.stock} title="Bestand">
|
||||||
<PanelValue>
|
<PanelValue>
|
||||||
{articleData.stock != null ? formatStock(articleData.stock) : null}
|
{articleData.stock != null ? formatStock(articleData.stock) : null}
|
||||||
</PanelValue>
|
</PanelValue>
|
||||||
</PanelTile>
|
</PanelTile>
|
||||||
|
|
||||||
<PanelTile wide={1} title="Marke">
|
<PanelTile wide={1} tileKey={PANEL_TILE_KEYS.brand} title="Marke">
|
||||||
<PanelValue>{brandName(articleData)}</PanelValue>
|
<PanelValue>{brandName(articleData)}</PanelValue>
|
||||||
</PanelTile>
|
</PanelTile>
|
||||||
|
|
||||||
<PanelTile wide={1} title="SKU / EAN">
|
<PanelTile wide={1} tileKey={PANEL_TILE_KEYS.identifiers} title="SKU / EAN">
|
||||||
<PanelValue label="SKU">{articleData.sku}</PanelValue>
|
<PanelValue label="SKU">{articleData.sku}</PanelValue>
|
||||||
{eans.map((ean) => (
|
{eans.map((ean) => (
|
||||||
<PanelValue key={ean} label="EAN">
|
<PanelValue key={ean} label="EAN">
|
||||||
@@ -168,7 +169,7 @@ export default function ArticleView({ article: articleData, onArticleUpdated })
|
|||||||
))}
|
))}
|
||||||
</PanelTile>
|
</PanelTile>
|
||||||
|
|
||||||
<PanelTile wide={1} title="Preis">
|
<PanelTile wide={1} tileKey={PANEL_TILE_KEYS.price} title="Preis">
|
||||||
<PanelValue label="Netto">
|
<PanelValue label="Netto">
|
||||||
{basePrice?.net != null ? formatPrice(basePrice.net) : null}
|
{basePrice?.net != null ? formatPrice(basePrice.net) : null}
|
||||||
</PanelValue>
|
</PanelValue>
|
||||||
@@ -180,14 +181,14 @@ export default function ArticleView({ article: articleData, onArticleUpdated })
|
|||||||
</PanelValue>
|
</PanelValue>
|
||||||
</PanelTile>
|
</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="Slug">{articleData.slug}</PanelValue>
|
||||||
<PanelValue label="SEO-Titel">{articleData.seoTitle}</PanelValue>
|
<PanelValue label="SEO-Titel">{articleData.seoTitle}</PanelValue>
|
||||||
<PanelValue label="SEO-Beschreibung">{articleData.seoDescription}</PanelValue>
|
<PanelValue label="SEO-Beschreibung">{articleData.seoDescription}</PanelValue>
|
||||||
<PanelValue label="Beschreibung">{articleData.description}</PanelValue>
|
<PanelValue label="Beschreibung">{articleData.description}</PanelValue>
|
||||||
</PanelTile>
|
</PanelTile>
|
||||||
|
|
||||||
<PanelTile wide={1} title="Listen" minHeight={200}>
|
<PanelTile wide={1} tileKey={PANEL_TILE_KEYS.lists} title="Listen" minHeight={200}>
|
||||||
<RelationLine
|
<RelationLine
|
||||||
label="Vaterartikel"
|
label="Vaterartikel"
|
||||||
active={articleData.childCount > 0}
|
active={articleData.childCount > 0}
|
||||||
|
|||||||
@@ -8,6 +8,7 @@ import CircularProgress from '@mui/material/CircularProgress';
|
|||||||
import IconButton from '@mui/material/IconButton';
|
import IconButton from '@mui/material/IconButton';
|
||||||
import Paper from '@mui/material/Paper';
|
import Paper from '@mui/material/Paper';
|
||||||
import Typography from '@mui/material/Typography';
|
import Typography from '@mui/material/Typography';
|
||||||
|
import { injectMuiTheme } from './injectMuiTheme';
|
||||||
import { enqueueSnackbar } from 'notistack';
|
import { enqueueSnackbar } from 'notistack';
|
||||||
import { backendErrorFrom } from '../../utils/backendError';
|
import { backendErrorFrom } from '../../utils/backendError';
|
||||||
import { panelTilePaperSx } from './panelTileLayout';
|
import { panelTilePaperSx } from './panelTileLayout';
|
||||||
@@ -20,7 +21,7 @@ function valuesEqual(a, b) {
|
|||||||
* Panel tile with centralized inline-edit chrome.
|
* Panel tile with centralized inline-edit chrome.
|
||||||
* Pass `onSave` to enable edit UI; omit it for read-only tiles (no edit icon).
|
* 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) {
|
constructor(props) {
|
||||||
super(props);
|
super(props);
|
||||||
this.inputRef = createRef();
|
this.inputRef = createRef();
|
||||||
@@ -186,9 +187,11 @@ export class EditablePanelTile extends Component {
|
|||||||
|
|
||||||
render() {
|
render() {
|
||||||
const {
|
const {
|
||||||
|
theme,
|
||||||
wide = 1,
|
wide = 1,
|
||||||
sx,
|
sx,
|
||||||
minHeight = 120,
|
minHeight = 120,
|
||||||
|
tileKey,
|
||||||
renderDisplay,
|
renderDisplay,
|
||||||
renderEdit,
|
renderEdit,
|
||||||
} = this.props;
|
} = this.props;
|
||||||
@@ -198,8 +201,15 @@ export class EditablePanelTile extends Component {
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<Paper
|
<Paper
|
||||||
variant="outlined"
|
elevation={0}
|
||||||
sx={panelTilePaperSx({ wide, sx, minHeight, pending })}
|
sx={panelTilePaperSx({
|
||||||
|
wide,
|
||||||
|
sx,
|
||||||
|
minHeight,
|
||||||
|
pending,
|
||||||
|
tileKey,
|
||||||
|
theme,
|
||||||
|
})}
|
||||||
>
|
>
|
||||||
{this.renderActions()}
|
{this.renderActions()}
|
||||||
{this.renderTitle()}
|
{this.renderTitle()}
|
||||||
@@ -220,3 +230,5 @@ export class EditablePanelTile extends Component {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export const EditablePanelTile = injectMuiTheme(EditablePanelTileBase);
|
||||||
|
|||||||
@@ -2,9 +2,10 @@ import { Component } from 'react';
|
|||||||
import Box from '@mui/material/Box';
|
import Box from '@mui/material/Box';
|
||||||
import Paper from '@mui/material/Paper';
|
import Paper from '@mui/material/Paper';
|
||||||
import Typography from '@mui/material/Typography';
|
import Typography from '@mui/material/Typography';
|
||||||
|
import { injectMuiTheme } from './injectMuiTheme';
|
||||||
import { GRID_COLUMNS, panelTilePaperSx } from './panelTileLayout';
|
import { GRID_COLUMNS, panelTilePaperSx } from './panelTileLayout';
|
||||||
|
|
||||||
export class PanelTile extends Component {
|
class PanelTileBase extends Component {
|
||||||
renderTitle() {
|
renderTitle() {
|
||||||
const { title } = this.props;
|
const { title } = this.props;
|
||||||
if (!title) return null;
|
if (!title) return null;
|
||||||
@@ -20,12 +21,18 @@ export class PanelTile extends Component {
|
|||||||
}
|
}
|
||||||
|
|
||||||
render() {
|
render() {
|
||||||
const { wide = 1, children, sx, minHeight = 120 } = this.props;
|
const { theme, wide = 1, children, sx, minHeight = 120, tileKey } = this.props;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Paper
|
<Paper
|
||||||
variant="outlined"
|
elevation={0}
|
||||||
sx={panelTilePaperSx({ wide, sx, minHeight })}
|
sx={panelTilePaperSx({
|
||||||
|
wide,
|
||||||
|
sx,
|
||||||
|
minHeight,
|
||||||
|
tileKey,
|
||||||
|
theme,
|
||||||
|
})}
|
||||||
>
|
>
|
||||||
{this.renderTitle()}
|
{this.renderTitle()}
|
||||||
<Box sx={{ flex: 1, minWidth: 0 }}>{children}</Box>
|
<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 }) {
|
export function PanelTileGrid({ children, sx }) {
|
||||||
return (
|
return (
|
||||||
<Box
|
<Box
|
||||||
|
|||||||
13
src/components/panels/injectMuiTheme.jsx
Normal file
13
src/components/panels/injectMuiTheme.jsx
Normal 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;
|
||||||
|
}
|
||||||
@@ -17,7 +17,21 @@ export function responsiveColumnSpan(wide) {
|
|||||||
|
|
||||||
export const PENDING_EDIT_RING_PX = 4;
|
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 {
|
return {
|
||||||
gridColumn: responsiveColumnSpan(wide),
|
gridColumn: responsiveColumnSpan(wide),
|
||||||
position: 'relative',
|
position: 'relative',
|
||||||
@@ -25,6 +39,9 @@ export function panelTilePaperSx({ wide, sx, minHeight = 120, pending = false })
|
|||||||
minHeight,
|
minHeight,
|
||||||
display: 'flex',
|
display: 'flex',
|
||||||
flexDirection: 'column',
|
flexDirection: 'column',
|
||||||
|
border: 'none',
|
||||||
|
boxShadow: 'none',
|
||||||
|
...(tileBg ? { bgcolor: tileBg } : {}),
|
||||||
...(pending
|
...(pending
|
||||||
? {
|
? {
|
||||||
boxShadow: `inset 0 0 0 ${PENDING_EDIT_RING_PX}px ${PENDING_EDIT_BORDER}`,
|
boxShadow: `inset 0 0 0 ${PENDING_EDIT_RING_PX}px ${PENDING_EDIT_BORDER}`,
|
||||||
|
|||||||
@@ -2,7 +2,7 @@ html,
|
|||||||
body,
|
body,
|
||||||
#root {
|
#root {
|
||||||
min-height: 100%;
|
min-height: 100%;
|
||||||
background-color: #dceeea;
|
background-color: #c5ddd8;
|
||||||
}
|
}
|
||||||
|
|
||||||
html,
|
html,
|
||||||
|
|||||||
@@ -6,7 +6,7 @@ import { SnackbarProvider } from 'notistack';
|
|||||||
import AuthBootstrap from './components/AuthBootstrap';
|
import AuthBootstrap from './components/AuthBootstrap';
|
||||||
import App from './App';
|
import App from './App';
|
||||||
import { store } from './store';
|
import { store } from './store';
|
||||||
import theme from './theme';
|
import { getActiveTheme } from './theme';
|
||||||
import './index.css';
|
import './index.css';
|
||||||
|
|
||||||
async function clearDevServiceWorker() {
|
async function clearDevServiceWorker() {
|
||||||
@@ -25,7 +25,7 @@ if (import.meta.env.DEV) {
|
|||||||
|
|
||||||
createRoot(document.getElementById('root')).render(
|
createRoot(document.getElementById('root')).render(
|
||||||
<Provider store={store}>
|
<Provider store={store}>
|
||||||
<ThemeProvider theme={theme}>
|
<ThemeProvider theme={getActiveTheme()}>
|
||||||
<CssBaseline />
|
<CssBaseline />
|
||||||
<SnackbarProvider
|
<SnackbarProvider
|
||||||
maxSnack={3}
|
maxSnack={3}
|
||||||
|
|||||||
25
src/theme.js
25
src/theme.js
@@ -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
14
src/theme/index.js
Normal 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
11
src/theme/panelTiles.js
Normal 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
38
src/theme/themeManager.js
Normal 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;
|
||||||
|
}
|
||||||
42
src/theme/themes/default.js
Normal file
42
src/theme/themes/default.js
Normal 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,
|
||||||
|
});
|
||||||
|
}
|
||||||
@@ -134,7 +134,7 @@ export default defineConfig(({ command }) => ({
|
|||||||
start_url: '/',
|
start_url: '/',
|
||||||
scope: '/',
|
scope: '/',
|
||||||
display: 'standalone',
|
display: 'standalone',
|
||||||
background_color: '#dceeea',
|
background_color: '#c5ddd8',
|
||||||
theme_color: '#3b5bdb',
|
theme_color: '#3b5bdb',
|
||||||
icons: [
|
icons: [
|
||||||
{
|
{
|
||||||
|
|||||||
Reference in New Issue
Block a user