From 7cd053ada6c6706d9ff2a85e553aabf741a0160c Mon Sep 17 00:00:00 2001 From: seb Date: Fri, 31 Jul 2026 19:38:52 +0200 Subject: [PATCH] u --- src/components/AppShell.jsx | 4 +- src/components/CompanySettingsDialog.jsx | 6 +- src/components/NameDialog.jsx | 8 +-- src/components/PageLayout.jsx | 76 ++++++++++---------- src/components/SingleLineTextEdit.jsx | 62 ++++++++++++++++ src/components/article/ArticleView.jsx | 79 ++++++++++++--------- src/components/panels/EditablePanelTile.jsx | 75 +++++++++++-------- src/components/panels/panelTileLayout.js | 18 ++++- src/nav.js | 10 +++ 9 files changed, 221 insertions(+), 117 deletions(-) create mode 100644 src/components/SingleLineTextEdit.jsx diff --git a/src/components/AppShell.jsx b/src/components/AppShell.jsx index 16a7706..741031b 100644 --- a/src/components/AppShell.jsx +++ b/src/components/AppShell.jsx @@ -5,7 +5,7 @@ import ListItemIcon from '@mui/material/ListItemIcon'; import ListItemText from '@mui/material/ListItemText'; import { Outlet, useLocation, useNavigate } from 'react-router-dom'; import UserMenu from './UserMenu'; -import { NAV_ITEMS } from '../nav'; +import { NAV_ITEMS, isNavItemActive } from '../nav'; import { artikelQueryPath } from '../pages/artikel/artikelQuery'; const SIDEBAR_WIDTH = 144; @@ -28,7 +28,7 @@ export default function AppShell() { > {NAV_ITEMS.map(({ path, label, icon: Icon }) => { - const active = pathname === path || pathname.startsWith(`${path}/`); + const active = isNavItemActive(pathname, path); return ( ) : ( - this.setState({ name: e.target.value })} + onChange={(nextName) => this.setState({ name: nextName })} autoComplete="organization" autoFocus /> diff --git a/src/components/NameDialog.jsx b/src/components/NameDialog.jsx index 258c55d..f3c5e7d 100644 --- a/src/components/NameDialog.jsx +++ b/src/components/NameDialog.jsx @@ -5,9 +5,9 @@ import Dialog from '@mui/material/Dialog'; import DialogActions from '@mui/material/DialogActions'; import DialogContent from '@mui/material/DialogContent'; import DialogTitle from '@mui/material/DialogTitle'; -import TextField from '@mui/material/TextField'; import Alert from '@mui/material/Alert'; import BackendError from './BackendError'; +import SingleLineTextEdit from './SingleLineTextEdit'; import { backendErrorFrom, isBackendFailure } from '../utils/backendError'; /** @@ -79,15 +79,13 @@ class NameDialog extends Component { ) ) : null} - this.setState({ name: e.target.value })} - autoComplete="off" + onChange={(nextName) => this.setState({ name: nextName })} /> diff --git a/src/components/PageLayout.jsx b/src/components/PageLayout.jsx index 47347e2..a63d5bb 100644 --- a/src/components/PageLayout.jsx +++ b/src/components/PageLayout.jsx @@ -1,6 +1,5 @@ import Box from '@mui/material/Box'; import IconButton from '@mui/material/IconButton'; -import Tooltip from '@mui/material/Tooltip'; import Typography from '@mui/material/Typography'; import { useLocation, useNavigate } from 'react-router-dom'; import { artikelQueryPath } from '../pages/artikel/artikelQuery'; @@ -8,11 +7,10 @@ import { defaultCategoryPath, isKategorienPath } from '../pages/artikel/category const SUB_NAV_WIDTH = 80; -function isSubNavActive(pathname, basePath, segment, knownSegments) { +function isSubNavActive(pathname, basePath, segment) { if (!segment) { - if (pathname === basePath || pathname === `${basePath}/`) return true; - const rest = pathname.slice(basePath.length).replace(/^\//, ''); - return Boolean(rest) && !rest.includes('/') && !knownSegments.has(rest); + if (isKategorienPath(pathname)) return false; + return pathname === basePath || pathname === `${basePath}/`; } if (segment === 'kategorien') { return isKategorienPath(pathname); @@ -25,7 +23,6 @@ export default function PageLayout({ title, showTitle = true, subNav, basePath, const hasSubNav = subNav?.length > 0; const navigate = useNavigate(); const { pathname } = useLocation(); - const knownSegments = new Set((subNav ?? []).map((item) => item.segment).filter(Boolean)); return ( @@ -43,42 +40,41 @@ export default function PageLayout({ title, showTitle = true, subNav, basePath, > {hasSubNav && subNav.map(({ segment, label, icon: Icon }) => { - const active = isSubNavActive(pathname, basePath, segment, knownSegments); + const active = isSubNavActive(pathname, basePath, segment); return ( - - e.preventDefault()} - onClick={() => { - if (!segment && basePath === '/artikel') { - navigate(artikelQueryPath()); - return; - } - if (segment === 'kategorien') { - navigate(defaultCategoryPath()); - return; - } - navigate(segment ? `${basePath}/${segment}` : basePath); - }} - sx={(theme) => ({ - width: 80, - height: 80, - borderRadius: '50%', - color: active ? theme.palette.primary.main : theme.palette.text.secondary, - bgcolor: 'transparent', - '&:hover': { - bgcolor: theme.palette.action.hover, - color: theme.palette.primary.main, - }, - })} - > - - - + e.preventDefault()} + onClick={() => { + if (!segment && basePath === '/artikel') { + navigate(artikelQueryPath()); + return; + } + if (segment === 'kategorien') { + navigate(defaultCategoryPath()); + return; + } + navigate(segment ? `${basePath}/${segment}` : basePath); + }} + sx={(theme) => ({ + width: 80, + height: 80, + borderRadius: '50%', + color: active ? theme.palette.primary.main : theme.palette.text.secondary, + bgcolor: 'transparent', + '&:hover': { + bgcolor: theme.palette.action.hover, + color: theme.palette.primary.main, + }, + })} + > + + ); })} diff --git a/src/components/SingleLineTextEdit.jsx b/src/components/SingleLineTextEdit.jsx new file mode 100644 index 0000000..50bc35d --- /dev/null +++ b/src/components/SingleLineTextEdit.jsx @@ -0,0 +1,62 @@ +import { Component } from 'react'; +import TextField from '@mui/material/TextField'; + +/** + * Centralized single-line name field — same outlined look everywhere. + * Props: value, onChange(nextValue), plus optional TextField props. + */ +class SingleLineTextEdit extends Component { + render() { + const { + value, + onChange, + inputRef, + label = 'Name', + disabled = false, + readOnly = false, + autoFocus = false, + autoComplete = 'off', + size, + fullWidth = true, + required = false, + sx, + slotProps, + ...rest + } = this.props; + + return ( + { + if (!readOnly) onChange(event.target.value); + }} + autoFocus={autoFocus} + autoComplete={autoComplete} + slotProps={{ + ...slotProps, + input: { + readOnly, + ...slotProps?.input, + }, + htmlInput: { + spellCheck: false, + readOnly, + tabIndex: readOnly ? -1 : undefined, + ...slotProps?.htmlInput, + }, + }} + sx={sx} + {...rest} + /> + ); + } +} + +export default SingleLineTextEdit; diff --git a/src/components/article/ArticleView.jsx b/src/components/article/ArticleView.jsx index 6f4fb41..8a3830b 100644 --- a/src/components/article/ArticleView.jsx +++ b/src/components/article/ArticleView.jsx @@ -1,8 +1,8 @@ import { Link as RouterLink } from 'react-router-dom'; import Box from '@mui/material/Box'; import Link from '@mui/material/Link'; -import TextField from '@mui/material/TextField'; import Typography from '@mui/material/Typography'; +import SingleLineTextEdit from '../SingleLineTextEdit'; import { article } from '../../api'; import { EditablePanelTile, PanelTile, PanelTileGrid } from '../panels'; import { requireObject } from '../../utils/apiShape'; @@ -82,6 +82,35 @@ function nameDraftValid(draft) { return draft.name.trim().length > 0; } +const articleNameTextSx = { + fontWeight: 700, + lineHeight: 1.2, +}; + +const articleNameFieldSx = { + '& .MuiInputBase-input': { + fontSize: '2.125rem', + ...articleNameTextSx, + }, +}; + +const articleNameFieldHiddenSx = { + ...articleNameFieldSx, + pointerEvents: 'none', + '& .MuiOutlinedInput-notchedOutline': { + borderColor: 'transparent', + }, + '& .MuiOutlinedInput-root:hover .MuiOutlinedInput-notchedOutline': { + borderColor: 'transparent', + }, + '& .MuiOutlinedInput-root.Mui-focused .MuiOutlinedInput-notchedOutline': { + borderColor: 'transparent', + }, + '& .MuiInputLabel-root': { + visibility: 'hidden', + }, +}; + export default function ArticleView({ article: articleData, onArticleUpdated }) { const basePrice = primaryPrice(articleData); const brutto = priceBrutto(basePrice); @@ -107,42 +136,22 @@ export default function ArticleView({ article: articleData, onArticleUpdated }) isDirty={nameDraftDirty} canSave={nameDraftValid} renderDisplay={({ name }) => ( - - - {name} - - + {}} + sx={articleNameFieldHiddenSx} + /> )} renderEdit={(draft, onChange, inputRef) => ( - - onChange({ name: event.target.value })} - sx={{ - '& .MuiInput-input': { - fontSize: '1.25rem', - fontWeight: 700, - lineHeight: 1.25, - }, - }} - /> - + onChange({ name })} + sx={articleNameFieldSx} + /> )} /> diff --git a/src/components/panels/EditablePanelTile.jsx b/src/components/panels/EditablePanelTile.jsx index e9347fd..69353b0 100644 --- a/src/components/panels/EditablePanelTile.jsx +++ b/src/components/panels/EditablePanelTile.jsx @@ -17,6 +17,8 @@ function valuesEqual(a, b) { return JSON.stringify(a) === JSON.stringify(b); } +const ACTION_PADDING = 12; + /** * Panel tile with centralized inline-edit chrome. * Pass `onSave` to enable edit UI; omit it for read-only tiles (no edit icon). @@ -120,14 +122,13 @@ class EditablePanelTileBase extends Component { }; renderTitle() { - const { title } = this.props; - const { editing } = this.state; + const { title, onSave } = this.props; if (!title) return null; return ( {title} @@ -148,32 +149,39 @@ class EditablePanelTileBase extends Component { if (!editing) { return ( - - - + <> + + + + + + + ); } return ( <> - - - {saving ? ( @@ -181,23 +189,25 @@ class EditablePanelTileBase extends Component { )} + + + ); } render() { - const { - theme, - wide = 1, - sx, - minHeight = 120, - tileKey, - renderDisplay, - renderEdit, - } = this.props; + const { onSave, theme, wide = 1, sx, minHeight = 120, tileKey, renderDisplay, renderEdit } = + this.props; const { editing, draft, error } = this.state; const savedValue = this.getSavedValue(); const pending = this.isDirty(); + const actionPadding = onSave ? ACTION_PADDING : 0; return ( {editing && draft != null diff --git a/src/components/panels/panelTileLayout.js b/src/components/panels/panelTileLayout.js index 8b48451..96c8df2 100644 --- a/src/components/panels/panelTileLayout.js +++ b/src/components/panels/panelTileLayout.js @@ -1,6 +1,7 @@ const GRID_COLUMNS = 4; -export const PENDING_EDIT_BORDER = '#ec4899'; +export const PENDING_EDIT_BORDER = '#e11d48'; +export const EDITING_TILE_BORDER = '#94a3b8'; function clampWide(wide) { return Math.min(Math.max(wide, 1), 3); @@ -24,10 +25,16 @@ export function panelTileBackground(theme, tileKey) { export const PANEL_TILE_BORDER_RADIUS = 4; +function tileRingBoxShadow(ringColor, elevationShadow) { + const inset = `inset 0 0 0 ${PENDING_EDIT_RING_PX}px ${ringColor}`; + return elevationShadow ? `${inset}, ${elevationShadow}` : inset; +} + export function panelTilePaperSx({ wide, sx, minHeight = 120, + editing = false, pending = false, tileKey, theme, @@ -35,6 +42,11 @@ export function panelTilePaperSx({ const tileBg = panelTileBackground(theme, tileKey); const elevationShadow = theme?.shadows?.[4]; + const ringColor = pending + ? PENDING_EDIT_BORDER + : editing + ? EDITING_TILE_BORDER + : null; return { gridColumn: responsiveColumnSpan(wide), @@ -46,9 +58,9 @@ export function panelTilePaperSx({ border: 'none', borderRadius: PANEL_TILE_BORDER_RADIUS, ...(tileBg ? { bgcolor: tileBg } : {}), - ...(pending && elevationShadow + ...(ringColor ? { - boxShadow: `inset 0 0 0 ${PENDING_EDIT_RING_PX}px ${PENDING_EDIT_BORDER}, ${elevationShadow}`, + boxShadow: tileRingBoxShadow(ringColor, elevationShadow), } : {}), ...sx, diff --git a/src/nav.js b/src/nav.js index c7ea060..6ebcf42 100644 --- a/src/nav.js +++ b/src/nav.js @@ -10,6 +10,7 @@ import ListAltIcon from '@mui/icons-material/ListAlt'; import ArchiveIcon from '@mui/icons-material/Archive'; import GroupsIcon from '@mui/icons-material/Groups'; import ContactPageIcon from '@mui/icons-material/ContactPage'; +import { isKategorienPath } from './pages/artikel/categoryRoute'; export const NAV_ITEMS = [ { path: '/artikel', label: 'Artikel', icon: Inventory2Icon }, @@ -19,6 +20,15 @@ export const NAV_ITEMS = [ { path: '/infos', label: 'Infos', icon: InfoIcon }, ]; +export function isNavItemActive(pathname, navPath) { + if (navPath === '/artikel') { + return pathname === navPath + || pathname.startsWith(`${navPath}/`) + || isKategorienPath(pathname); + } + return pathname === navPath || pathname.startsWith(`${navPath}/`); +} + export const SUB_NAV = { '/artikel': [ { segment: '', icon: ViewListIcon },