u
This commit is contained in:
@@ -5,15 +5,18 @@
|
||||
<meta charset="UTF-8" />
|
||||
<link rel="icon" type="image/svg+xml" href="/favicon.svg" />
|
||||
<link rel="apple-touch-icon" href="/icons/icon-192.png" />
|
||||
<meta name="theme-color" content="#3b5bdb" />
|
||||
<meta name="theme-color" content="#1d4ed8" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0, maximum-scale=1.0, user-scalable=no" />
|
||||
<title>Dashfront</title>
|
||||
<style>
|
||||
/* First-paint colors. Mirrored from src/colors.js (MAIN_BACKGROUND /
|
||||
THEME_COLOR) because this file must paint before the JS bundle loads.
|
||||
Once the app boots, src/colors.js takes over via CSS custom properties. */
|
||||
html,
|
||||
body,
|
||||
#root {
|
||||
min-height: 100%;
|
||||
background-color: #85b7ad;
|
||||
background-color: #82b8b4;
|
||||
}
|
||||
|
||||
html,
|
||||
|
||||
348
src/colors.js
348
src/colors.js
@@ -1,2 +1,346 @@
|
||||
// Mirrored in index.html and index.css for paint before JS loads.
|
||||
export const MAIN_BACKGROUND = '#85b7ad';
|
||||
/**
|
||||
* ============================================================================
|
||||
* CENTRAL COLOR PALETTE — the single source of truth for `dashfront`.
|
||||
* ============================================================================
|
||||
*
|
||||
* The palette is intentionally tiny: 1 white + 6 light greys + 6 dark greys
|
||||
* + 7 chromatic families (each a `dark`/`light` pair) = 20 base values.
|
||||
* There is no black: the darkest neutral is `DARK_6`, and every shadow or
|
||||
* scrim is an alpha of it (see `shade()`).
|
||||
*
|
||||
* EVERYTHING ELSE IN THIS FILE IS DERIVED from those 20 values. Change one
|
||||
* base and the whole app follows, live, via Vite HMR.
|
||||
*
|
||||
* HOW TO PLAY WITH THE COLORS
|
||||
* ---------------------------
|
||||
* 1. Edit any of the 20 base values in section 1.
|
||||
* 2. `CHROMA.<name>.dark` → text, icons, borders, solid buttons.
|
||||
* `CHROMA.<name>.light` → panel fills, chips, tints.
|
||||
*
|
||||
* RULES FOR NEW CODE
|
||||
* ------------------
|
||||
* * Never write a hex/rgba literal in a component — import a token.
|
||||
* * Prefer a base token. Reach for a derived one only when it reads better.
|
||||
*/
|
||||
|
||||
/* ────────────────────────────────────────────────────────────────────────────
|
||||
* 1. THE 20 BASE VALUES
|
||||
* -------------------------------------------------------------------------*/
|
||||
|
||||
/** Pure white — cards, dialogs, page panels. */
|
||||
export const WHITE = '#ffffff';
|
||||
|
||||
/** Light greys, ascending darkness. `LIGHT_1` is the faintest surface. */
|
||||
export const LIGHT_1 = '#fcfcfc';
|
||||
export const LIGHT_2 = '#fafafa';
|
||||
export const LIGHT_3 = '#f8fafc';
|
||||
export const LIGHT_4 = '#f1f5f9';
|
||||
export const LIGHT_5 = '#e2e8f0';
|
||||
export const LIGHT_6 = '#cbd5e1';
|
||||
|
||||
/** Dark greys, descending lightness. `DARK_1` is muted text, `DARK_6` darkest. */
|
||||
export const DARK_1 = '#94a3b8';
|
||||
export const DARK_2 = '#64748b';
|
||||
export const DARK_3 = '#475569';
|
||||
export const DARK_4 = '#334155';
|
||||
export const DARK_5 = '#1e293b';
|
||||
export const DARK_6 = '#0f172a';
|
||||
|
||||
/**
|
||||
* The seven chromatic families.
|
||||
* dark → text, icons, borders, solid buttons (readable on white)
|
||||
* light → panel fills, chips, tints
|
||||
*/
|
||||
export const CHROMA = {
|
||||
RED: { dark: '#b91c1c', light: '#fecaca' },
|
||||
ORANGE: { dark: '#d97706', light: '#fed7aa' },
|
||||
YELLOW: { dark: '#ca8a04', light: '#fde68a' },
|
||||
GREEN: { dark: '#15803d', light: '#bbf7d0' },
|
||||
PETROL: { dark: '#0f766e', light: '#99f6e4' },
|
||||
BLUE: { dark: '#1d4ed8', light: '#bfdbfe' },
|
||||
PURPLE: { dark: '#6d28d9', light: '#ddd6fe' },
|
||||
};
|
||||
|
||||
/* ────────────────────────────────────────────────────────────────────────────
|
||||
* 2. COLOR MATH (the only functions in this file)
|
||||
* -------------------------------------------------------------------------*/
|
||||
|
||||
function toHex(parts) {
|
||||
return `#${parts
|
||||
.map((v) => Math.max(0, Math.min(255, Math.round(v))).toString(16).padStart(2, '0'))
|
||||
.join('')}`;
|
||||
}
|
||||
|
||||
function channels(hex) {
|
||||
return [1, 3, 5].map((i) => parseInt(hex.slice(i, i + 2), 16));
|
||||
}
|
||||
|
||||
/** Linear blend: `amount` 0 → `a`, 1 → `b`. */
|
||||
export function mix(a, b, amount) {
|
||||
const ca = channels(a);
|
||||
const cb = channels(b);
|
||||
return toHex(ca.map((v, i) => v + (cb[i] - v) * amount));
|
||||
}
|
||||
|
||||
/** `color` at `opacity`, as an `rgba()` string. */
|
||||
export function alpha(color, opacity) {
|
||||
const [r, g, b] = channels(color);
|
||||
return `rgba(${r}, ${g}, ${b}, ${opacity})`;
|
||||
}
|
||||
|
||||
/** An alpha of `DARK_6` — our stand-in for "black". */
|
||||
export const shade = (opacity) => alpha(DARK_6, opacity);
|
||||
|
||||
/** An alpha of `WHITE` — used for text and highlights over dark surfaces. */
|
||||
export const veil = (opacity) => alpha(WHITE, opacity);
|
||||
|
||||
/** A lighter tint of a chroma family: 0 → `light`, 1 → white. */
|
||||
const tint = (family, amount) => mix(CHROMA[family].light, WHITE, amount);
|
||||
|
||||
/** Resolve a `{ dark, light }` pair against the active MUI theme. */
|
||||
export function byMode(theme, pair, fallback) {
|
||||
if (theme.palette.mode === 'dark' && pair.dark != null) return pair.dark;
|
||||
return pair.light ?? fallback;
|
||||
}
|
||||
|
||||
/* ────────────────────────────────────────────────────────────────────────────
|
||||
* 3. BRAND & SHELL
|
||||
* -------------------------------------------------------------------------*/
|
||||
|
||||
/** App background — a soft petrol wash. */
|
||||
export const MAIN_BACKGROUND = mix(WHITE, CHROMA.PETROL.dark, 0.52);
|
||||
|
||||
/** MUI `palette.primary.main`. */
|
||||
export const PRIMARY = CHROMA.PETROL.dark;
|
||||
|
||||
/** MUI `palette.background.paper`. */
|
||||
export const PAPER = WHITE;
|
||||
|
||||
/** `meta[name="theme-color"]` in `index.html`. */
|
||||
export const THEME_COLOR = CHROMA.BLUE.dark;
|
||||
|
||||
/* ────────────────────────────────────────────────────────────────────────────
|
||||
* 4. NEUTRAL SURFACES
|
||||
* -------------------------------------------------------------------------*/
|
||||
|
||||
export const SURFACE_FAINT = LIGHT_1; // attachment cards
|
||||
export const SURFACE_SUBTLE = LIGHT_2; // placeholders, letterboxes
|
||||
export const SURFACE_BASE = LIGHT_3; // panels, tiles, default tint
|
||||
export const SURFACE_RAISED = LIGHT_4; // inset boxes, table heads, chips
|
||||
export const BORDER = LIGHT_5; // dividers, outlines, placeholders
|
||||
export const BORDER_STRONG = LIGHT_6; // muted rules, checkerboard
|
||||
|
||||
export const TEXT_MUTED = DARK_1;
|
||||
export const TEXT_SECONDARY = DARK_2;
|
||||
export const TEXT_BODY = DARK_3;
|
||||
export const TEXT_STRONG = DARK_4;
|
||||
export const INK = DARK_5; // barcode strokes
|
||||
export const INK_STRONG = DARK_6; // label text on light chips
|
||||
|
||||
/** Semi-transparent panel wash for empty states. */
|
||||
export const SURFACE_WASH = alpha(SURFACE_BASE, 0.7);
|
||||
|
||||
/** Neutral bubble / dialog fill that follows the active mode. */
|
||||
export const NEUTRAL_BUBBLE = { dark: WHITE, light: LIGHT_4 };
|
||||
|
||||
/* ────────────────────────────────────────────────────────────────────────────
|
||||
* 5. CHROMA DERIVATIONS
|
||||
* Every pastel used for panels, chips and tiles comes from here.
|
||||
* -------------------------------------------------------------------------*/
|
||||
|
||||
export const RED_BG = tint('RED', 0.85);
|
||||
export const RED_PANEL = CHROMA.RED.light;
|
||||
export const RED_SOFT = tint('RED', 0.35);
|
||||
export const RED = CHROMA.RED.dark;
|
||||
export const RED_HOVER = mix(CHROMA.RED.dark, CHROMA.ORANGE.dark, 0.5);
|
||||
export const RED_TINT = alpha(CHROMA.RED.dark, 0.12);
|
||||
export const RED_GLOW = alpha(CHROMA.RED.dark, 0.6);
|
||||
|
||||
export const ORANGE_BG = tint('ORANGE', 0.7);
|
||||
export const ORANGE_PANEL = CHROMA.ORANGE.light;
|
||||
export const ORANGE = CHROMA.ORANGE.dark;
|
||||
|
||||
export const YELLOW_BG = tint('YELLOW', 0.75);
|
||||
export const YELLOW_PANEL = tint('YELLOW', 0.4);
|
||||
export const YELLOW = CHROMA.YELLOW.dark;
|
||||
|
||||
export const GREEN_BG = tint('GREEN', 0.75);
|
||||
export const GREEN_PANEL = CHROMA.GREEN.light;
|
||||
export const GREEN_SOFT = tint('GREEN', 0.15);
|
||||
export const GREEN = CHROMA.GREEN.dark;
|
||||
|
||||
export const PETROL_BG = tint('PETROL', 0.85);
|
||||
export const PETROL_PANEL = CHROMA.PETROL.light;
|
||||
export const PETROL = CHROMA.PETROL.dark;
|
||||
|
||||
export const BLUE_BG = tint('BLUE', 0.85);
|
||||
export const BLUE_PANEL = CHROMA.BLUE.light;
|
||||
export const BLUE_SOFT = tint('BLUE', 0.25);
|
||||
export const BLUE = CHROMA.BLUE.dark;
|
||||
export const BLUE_GLOW = alpha(CHROMA.BLUE.dark, 0.6);
|
||||
export const BLUE_SHADOW = alpha(CHROMA.BLUE.dark, 0.25);
|
||||
export const BLUE_TINT = alpha(CHROMA.BLUE.dark, 0.12);
|
||||
export const BLUE_BORDER = alpha(CHROMA.BLUE.dark, 0.3);
|
||||
|
||||
export const PURPLE_BG = tint('PURPLE', 0.6);
|
||||
export const PURPLE_PANEL = CHROMA.PURPLE.light;
|
||||
export const PURPLE_SOFT = tint('PURPLE', 0.3);
|
||||
export const PURPLE = CHROMA.PURPLE.dark;
|
||||
|
||||
/** Between violet and red — used for the article list editor. */
|
||||
export const MAGENTA_PANEL = mix(CHROMA.PURPLE.light, CHROMA.RED.light, 0.45);
|
||||
|
||||
/** Between blue and petrol — used for brand text over photos. */
|
||||
export const ICE = mix(CHROMA.BLUE.light, CHROMA.PETROL.light, 0.4);
|
||||
|
||||
/** Transparent checkerboard behind image-search results. */
|
||||
export const CHECKERBOARD = `linear-gradient(45deg, ${BORDER_STRONG} 25%, transparent 25%), linear-gradient(-45deg, ${BORDER_STRONG} 25%, transparent 25%), linear-gradient(45deg, transparent 75%, ${BORDER_STRONG} 75%), linear-gradient(-45deg, transparent 75%, ${BORDER_STRONG} 75%)`;
|
||||
|
||||
/* ────────────────────────────────────────────────────────────────────────────
|
||||
* 6. STATUS & FEEDBACK
|
||||
* -------------------------------------------------------------------------*/
|
||||
|
||||
export const ERROR = RED;
|
||||
export const ERROR_HOVER = mix(RED, INK, 0.35);
|
||||
export const LINK = BLUE;
|
||||
|
||||
/** Selection state for list rows. */
|
||||
export const SELECT_BG = BLUE_TINT;
|
||||
export const SELECT_BG_HOVER = alpha(CHROMA.BLUE.dark, 0.16);
|
||||
export const SELECT_BORDER = BLUE_BORDER;
|
||||
|
||||
/** SortableJS drag ghost — also published as a CSS variable. */
|
||||
export const SORT_GHOST = GREEN;
|
||||
export const SORT_GHOST_BG = alpha(CHROMA.GREEN.dark, 0.12);
|
||||
|
||||
/** "Processing…" ribbon on image search results. */
|
||||
export const INFO_RIBBON = alpha(CHROMA.BLUE.dark, 0.92);
|
||||
|
||||
/** Delicate fills used on top of panel colors. */
|
||||
export const TINT_FAINT = shade(0.01);
|
||||
export const TINT_HAIRLINE = shade(0.03);
|
||||
export const TINT_SUBTLE = shade(0.04);
|
||||
export const TINT_ROW_HOVER = shade(0.05);
|
||||
export const TINT_CHIP = shade(0.06);
|
||||
export const TINT_CARD_HOVER = shade(0.06);
|
||||
export const TINT_PILL = shade(0.08);
|
||||
export const TINT_STRONG = shade(0.12);
|
||||
export const TINT_PILL_ACTIVE = shade(0.14);
|
||||
|
||||
/** Darkening for white text/icons sitting on imagery. */
|
||||
export const SCRIM_SOFT = shade(0.35);
|
||||
export const SCRIM_MEDIUM = shade(0.45);
|
||||
export const SCRIM_BADGE = shade(0.5);
|
||||
export const SCRIM_BUTTON = shade(0.55);
|
||||
export const SCRIM_ACTION = shade(0.6);
|
||||
export const SCRIM_LABEL = shade(0.65);
|
||||
export const SCRIM_STRONG = shade(0.75);
|
||||
export const SCRIM_HEAVY = shade(0.85);
|
||||
export const SCRIM_INK = shade(0.95);
|
||||
|
||||
/** White overlays: hover states on dark chrome, text on imagery. */
|
||||
export const VEIL_FAINT = veil(0.04);
|
||||
export const VEIL_SUBTLE = veil(0.05);
|
||||
export const VEIL_CHIP = veil(0.08);
|
||||
export const VEIL_HOVER = veil(0.15);
|
||||
export const VEIL_DISABLED = veil(0.3);
|
||||
export const VEIL_MUTED = veil(0.35);
|
||||
export const VEIL_PANEL = veil(0.55);
|
||||
export const VEIL_TEXT = veil(0.8);
|
||||
export const VEIL_CARD = veil(0.85);
|
||||
export const VEIL_CARD_STRONG = veil(0.9);
|
||||
export const VEIL_OVERLAY = veil(0.92);
|
||||
export const VEIL_SOLID = veil(1);
|
||||
|
||||
/* ────────────────────────────────────────────────────────────────────────────
|
||||
* 7. DARK-MODE PAIRS (resolve with `byMode(theme, PAIR)`)
|
||||
* -------------------------------------------------------------------------*/
|
||||
|
||||
export const SURFACE_SUBTLE_PAIR = { light: TINT_HAIRLINE, dark: VEIL_FAINT };
|
||||
export const TABLE_HEADER_PAIR = { light: TINT_SUBTLE, dark: VEIL_SUBTLE };
|
||||
export const CODE_INLINE_PAIR = { light: TINT_CHIP, dark: VEIL_CHIP };
|
||||
export const CODE_BLOCK_PAIR = { light: TINT_CHIP, dark: SCRIM_SOFT };
|
||||
export const ASK_PROMPT_BG_PAIR = { light: BLUE_BG, dark: BLUE_TINT };
|
||||
export const ASK_PROMPT_BORDER_PAIR = { light: BLUE_PANEL, dark: BLUE_BORDER };
|
||||
|
||||
/* ────────────────────────────────────────────────────────────────────────────
|
||||
* 8. SIDEBAR NAVIGATION
|
||||
* -------------------------------------------------------------------------*/
|
||||
|
||||
/** Deep petrol — the app's structural dark. */
|
||||
export const NAV_SURFACE = mix(DARK_6, CHROMA.PETROL.dark, 0.3);
|
||||
|
||||
/** Horizontal sheen across the sidebar. */
|
||||
export const NAV_BACKGROUND = `linear-gradient(90deg, ${mix(NAV_SURFACE, DARK_6, 0.4)} 0%, ${mix(NAV_SURFACE, WHITE, 0.06)} 25%, ${NAV_SURFACE} 60%, ${mix(NAV_SURFACE, DARK_6, 0.3)} 100%)`;
|
||||
|
||||
export const NAV_ITEM = mix(WHITE, CHROMA.PETROL.dark, 0.45);
|
||||
export const NAV_ITEM_HOVER = mix(DARK_5, CHROMA.PETROL.dark, 0.3);
|
||||
export const NAV_ITEM_HOVER_TEXT = mix(WHITE, CHROMA.PETROL.dark, 0.1);
|
||||
export const NAV_ITEM_ACTIVE = mix(DARK_5, CHROMA.PETROL.dark, 0.55);
|
||||
export const NAV_ITEM_ACTIVE_TEXT = mix(CHROMA.PETROL.light, WHITE, 0.15);
|
||||
|
||||
/** Accent for active items and live draft badges. */
|
||||
export const NAV_ITEM_ACTIVE_ACCENT = CHROMA.PETROL.light;
|
||||
|
||||
/* ────────────────────────────────────────────────────────────────────────────
|
||||
* 9. CHAT / AI ASSISTANT
|
||||
* -------------------------------------------------------------------------*/
|
||||
|
||||
export const CHAT = CHROMA.BLUE.dark;
|
||||
export const CHAT_BUSY = CHROMA.ORANGE.dark;
|
||||
export const CHAT_PENDING = mix(CHROMA.ORANGE.dark, CHROMA.RED.dark, 0.5);
|
||||
|
||||
/** Pulsing "assistant needs input" halo on the FAB. */
|
||||
export const CHAT_FAB_PENDING = `linear-gradient(135deg, ${CHAT_BUSY} 0%, ${CHAT_PENDING} 100%)`;
|
||||
/** Busy ("thinking") FAB. */
|
||||
export const CHAT_FAB_BUSY = `linear-gradient(135deg, ${CHAT_BUSY} 0%, ${mix(CHAT_BUSY, INK, 0.25)} 100%)`;
|
||||
|
||||
/** Matching drop shadows, so the FAB appears to float in the active color. */
|
||||
export const CHAT_FAB_SHADOW_IDLE = `0 8px 24px ${alpha(CHAT, 0.35)}`;
|
||||
export const CHAT_FAB_SHADOW_BUSY = `0 8px 24px ${alpha(CHAT_BUSY, 0.35)}`;
|
||||
export const CHAT_FAB_SHADOW_PENDING = `0 8px 24px ${alpha(CHAT_PENDING, 0.45)}`;
|
||||
|
||||
/* ────────────────────────────────────────────────────────────────────────────
|
||||
* 10. GRADIENTS & SHADOWS
|
||||
* -------------------------------------------------------------------------*/
|
||||
|
||||
/** Bottom scrims that keep captions legible over photos. */
|
||||
export const SCRIM_TILE = `linear-gradient(transparent, ${SCRIM_LABEL})`;
|
||||
export const SCRIM_ATTACHMENT = `linear-gradient(transparent, ${SCRIM_STRONG} 40%, ${SCRIM_HEAVY})`;
|
||||
|
||||
/** Text glow for tile captions drawn over imagery. */
|
||||
export const TEXT_GLOW_DARK = `0 1px 3px ${SCRIM_INK}, 0 0 2px ${SCRIM_HEAVY}`;
|
||||
export const TEXT_GLOW_LIGHT = `0 0 3px ${VEIL_SOLID}, 0 0 6px ${VEIL_CARD_STRONG}, 0 1px 2px ${VEIL_CARD_STRONG}`;
|
||||
|
||||
export const SHADOW_CARD = `0 1px 4px ${TINT_CARD_HOVER}`;
|
||||
export const SHADOW_DIALOG = `0 20px 40px ${shade(0.2)}`;
|
||||
export const SHADOW_POPOVER = `0 8px 28px ${shade(0.2)}`;
|
||||
export const SHADOW_BADGE = `0 2px 5px ${SCRIM_SOFT}`;
|
||||
export const SHADOW_DRAG = `0 8px 20px ${shade(0.28)}`;
|
||||
export const SHADOW_SORTABLE = `0 4px 16px ${TINT_STRONG}`;
|
||||
export const SHADOW_DRAWER = `-8px 0 32px ${TINT_STRONG}`;
|
||||
export const SHADOW_THUMB_RING = `0 0 0 1px ${TINT_STRONG}`;
|
||||
export const SHADOW_TAB = `drop-shadow(0px 2px 4px ${shade(0.2)}) drop-shadow(0px 4px 5px ${TINT_PILL_ACTIVE}) drop-shadow(0px 1px 10px ${TINT_STRONG})`;
|
||||
export const SHADOW_BUBBLE_USER = `0 2px 8px ${BLUE_SHADOW}`;
|
||||
export const SHADOW_BUBBLE_ASSISTANT = `0 1px 4px ${TINT_ROW_HOVER}`;
|
||||
|
||||
/* ────────────────────────────────────────────────────────────────────────────
|
||||
* 11. CSS VARIABLE BRIDGE
|
||||
* `src/index.css` cannot import this module, so these few tokens are
|
||||
* published as `var(--df-*)` custom properties by `installCssVars()`.
|
||||
* -------------------------------------------------------------------------*/
|
||||
|
||||
export const CSS_VARS = {
|
||||
'--df-main-background': MAIN_BACKGROUND,
|
||||
'--df-sort-ghost': SORT_GHOST,
|
||||
'--df-sort-ghost-tint': SORT_GHOST_BG,
|
||||
'--df-shadow-sortable': SHADOW_SORTABLE,
|
||||
'--df-shadow-drag': SHADOW_DRAG,
|
||||
};
|
||||
|
||||
/** Publish {@link CSS_VARS} onto `:root`. Call once at startup. */
|
||||
export function installCssVars(target = document.documentElement) {
|
||||
for (const [name, value] of Object.entries(CSS_VARS)) {
|
||||
target.style.setProperty(name, value);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -13,6 +13,9 @@ import { NAV_ITEMS, SUB_NAV, resolveActiveNav, resolveActiveSub } from '../nav';
|
||||
import { artikelQueryPath } from './article/artikelQuery';
|
||||
import { defaultCategoryPath } from './category/categoryRoute';
|
||||
import { navigationAutomat } from '../automats';
|
||||
import {
|
||||
NAV_SURFACE,
|
||||
} from '../colors';
|
||||
import { ChatFab, ChatDrawer } from './chat';
|
||||
|
||||
|
||||
@@ -51,7 +54,7 @@ class AppSidebar extends PureComponent {
|
||||
flexShrink: 0,
|
||||
display: 'flex',
|
||||
flexDirection: 'column',
|
||||
backgroundColor: theme.nav.backgroundColor || '#1a3533',
|
||||
backgroundColor: theme.nav.backgroundColor || NAV_SURFACE,
|
||||
background: theme.nav.background,
|
||||
position: 'sticky',
|
||||
top: 0,
|
||||
|
||||
@@ -8,6 +8,13 @@ import Popper from '@mui/material/Popper';
|
||||
import Typography from '@mui/material/Typography';
|
||||
import CloseIcon from '@mui/icons-material/Close';
|
||||
import { helpAutomat } from '../automats';
|
||||
import {
|
||||
RED,
|
||||
RED_HOVER,
|
||||
SHADOW_BADGE,
|
||||
SHADOW_POPOVER,
|
||||
WHITE,
|
||||
} from '../colors';
|
||||
|
||||
let nextHelpBadgeId = 0;
|
||||
|
||||
@@ -159,7 +166,7 @@ class HelpBadge extends PureComponent {
|
||||
borderRadius: 2,
|
||||
border: '1px solid',
|
||||
borderColor: 'divider',
|
||||
boxShadow: '0 8px 28px rgba(0,0,0,0.2)',
|
||||
boxShadow: SHADOW_POPOVER,
|
||||
}}
|
||||
>
|
||||
<Box
|
||||
@@ -177,8 +184,8 @@ class HelpBadge extends PureComponent {
|
||||
width: 18,
|
||||
height: 18,
|
||||
borderRadius: '50%',
|
||||
bgcolor: '#dc2626',
|
||||
color: '#ffffff',
|
||||
bgcolor: RED,
|
||||
color: WHITE,
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
justifyContent: 'center',
|
||||
@@ -246,15 +253,15 @@ class HelpBadge extends PureComponent {
|
||||
onClick={this.handleBadgeClick}
|
||||
onKeyDown={this.handleBadgeKeyDown}
|
||||
sx={{
|
||||
bgcolor: '#dc2626',
|
||||
color: '#ffffff',
|
||||
bgcolor: RED,
|
||||
color: WHITE,
|
||||
fontWeight: 800,
|
||||
fontSize: '0.78rem',
|
||||
width: 20,
|
||||
height: 20,
|
||||
minWidth: 20,
|
||||
borderRadius: '50%',
|
||||
boxShadow: '0 2px 5px rgba(0,0,0,0.35)',
|
||||
boxShadow: SHADOW_BADGE,
|
||||
cursor: 'pointer',
|
||||
display: 'inline-flex',
|
||||
alignItems: 'center',
|
||||
@@ -262,7 +269,7 @@ class HelpBadge extends PureComponent {
|
||||
userSelect: 'none',
|
||||
transition: 'background-color 0.15s ease',
|
||||
'&:hover': {
|
||||
bgcolor: '#b91c1c',
|
||||
bgcolor: RED_HOVER,
|
||||
},
|
||||
...badgeSx,
|
||||
...sx,
|
||||
@@ -308,20 +315,20 @@ class HelpBadge extends PureComponent {
|
||||
sx={{
|
||||
display: 'inline-flex',
|
||||
'& .MuiBadge-badge': {
|
||||
bgcolor: '#dc2626',
|
||||
color: '#ffffff',
|
||||
bgcolor: RED,
|
||||
color: WHITE,
|
||||
fontWeight: 800,
|
||||
fontSize: '0.78rem',
|
||||
width: 20,
|
||||
height: 20,
|
||||
minWidth: 20,
|
||||
borderRadius: '50%',
|
||||
boxShadow: '0 2px 5px rgba(0,0,0,0.35)',
|
||||
boxShadow: SHADOW_BADGE,
|
||||
cursor: 'pointer',
|
||||
p: 0,
|
||||
transition: 'background-color 0.15s ease',
|
||||
'&:hover': {
|
||||
bgcolor: '#b91c1c',
|
||||
bgcolor: RED_HOVER,
|
||||
},
|
||||
...badgeSx,
|
||||
},
|
||||
|
||||
@@ -17,6 +17,9 @@ import {
|
||||
} from '../auth/googleSignIn';
|
||||
|
||||
import quixposLogo from '../assets/quixpos-logo.png';
|
||||
import {
|
||||
NAV_SURFACE,
|
||||
} from '../colors';
|
||||
|
||||
class LoginPage extends PureComponent {
|
||||
constructor(props) {
|
||||
@@ -127,7 +130,7 @@ class LoginPage extends PureComponent {
|
||||
<Typography
|
||||
variant="h5"
|
||||
component="h1"
|
||||
sx={{ fontWeight: 700, color: '#1a3533', letterSpacing: '-0.01em' }}
|
||||
sx={{ fontWeight: 700, color: NAV_SURFACE, letterSpacing: '-0.01em' }}
|
||||
>
|
||||
QuixPOS
|
||||
</Typography>
|
||||
|
||||
@@ -29,6 +29,14 @@ import artikelAutomat from '../../automats/artikelAutomat';
|
||||
import { articleWithUploadedPhoto } from '../../utils/articleDetail';
|
||||
import { prepareArticlePhoto } from '../../utils/prepareArticlePhoto';
|
||||
import { requireObject } from '../../utils/apiShape';
|
||||
import {
|
||||
CHECKERBOARD,
|
||||
INFO_RIBBON,
|
||||
SCRIM_LABEL,
|
||||
VEIL_CARD,
|
||||
VEIL_OVERLAY,
|
||||
WHITE,
|
||||
} from '../../colors';
|
||||
|
||||
|
||||
function computeDefaultQuery(article) {
|
||||
@@ -502,9 +510,8 @@ export default class ArticleImageSearchDialog extends PureComponent {
|
||||
overflow: 'hidden',
|
||||
border: '1px solid',
|
||||
borderColor: isThisProcessing ? 'primary.main' : 'divider',
|
||||
bgcolor: '#fff',
|
||||
backgroundImage:
|
||||
'linear-gradient(45deg, #f0f0f0 25%, transparent 25%), linear-gradient(-45deg, #f0f0f0 25%, transparent 25%), linear-gradient(45deg, transparent 75%, #f0f0f0 75%), linear-gradient(-45deg, transparent 75%, #f0f0f0 75%)',
|
||||
bgcolor: WHITE,
|
||||
backgroundImage: CHECKERBOARD,
|
||||
backgroundSize: '16px 16px',
|
||||
backgroundPosition: '0 0, 0 8px, 8px -8px, -8px 0px',
|
||||
cursor: processingId ? 'default' : 'pointer',
|
||||
@@ -567,8 +574,8 @@ export default class ArticleImageSearchDialog extends PureComponent {
|
||||
height: 16,
|
||||
fontSize: '0.62rem',
|
||||
fontWeight: 600,
|
||||
bgcolor: 'rgba(0, 0, 0, 0.65)',
|
||||
color: '#fff',
|
||||
bgcolor: SCRIM_LABEL,
|
||||
color: WHITE,
|
||||
pointerEvents: 'none',
|
||||
px: 0.25,
|
||||
}}
|
||||
@@ -586,9 +593,9 @@ export default class ArticleImageSearchDialog extends PureComponent {
|
||||
right: 0,
|
||||
py: 0.6,
|
||||
px: 0.8,
|
||||
bgcolor: 'rgba(25, 118, 210, 0.92)',
|
||||
bgcolor: INFO_RIBBON,
|
||||
backdropFilter: 'blur(3px)',
|
||||
color: '#fff',
|
||||
color: WHITE,
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
justifyContent: 'center',
|
||||
@@ -615,7 +622,7 @@ export default class ArticleImageSearchDialog extends PureComponent {
|
||||
sx={{
|
||||
position: 'absolute',
|
||||
inset: 0,
|
||||
bgcolor: 'rgba(255, 255, 255, 0.92)',
|
||||
bgcolor: VEIL_OVERLAY,
|
||||
backdropFilter: 'blur(3px)',
|
||||
display: 'flex',
|
||||
flexDirection: 'column',
|
||||
@@ -662,10 +669,10 @@ export default class ArticleImageSearchDialog extends PureComponent {
|
||||
onClick={(e) => this.handleCopyUrl(img.url, e)}
|
||||
sx={{
|
||||
p: 0.4,
|
||||
bgcolor: 'rgba(255, 255, 255, 0.85)',
|
||||
bgcolor: VEIL_CARD,
|
||||
backdropFilter: 'blur(2px)',
|
||||
boxShadow: 1,
|
||||
'&:hover': { bgcolor: '#fff' },
|
||||
'&:hover': { bgcolor: WHITE },
|
||||
}}
|
||||
>
|
||||
<ContentCopyIcon sx={{ fontSize: 13, color: 'text.secondary' }} />
|
||||
@@ -677,10 +684,10 @@ export default class ArticleImageSearchDialog extends PureComponent {
|
||||
onClick={(e) => this.handleOpenUrl(img.url, e)}
|
||||
sx={{
|
||||
p: 0.4,
|
||||
bgcolor: 'rgba(255, 255, 255, 0.85)',
|
||||
bgcolor: VEIL_CARD,
|
||||
backdropFilter: 'blur(2px)',
|
||||
boxShadow: 1,
|
||||
'&:hover': { bgcolor: '#fff' },
|
||||
'&:hover': { bgcolor: WHITE },
|
||||
}}
|
||||
>
|
||||
<OpenInNewIcon sx={{ fontSize: 13, color: 'text.secondary' }} />
|
||||
|
||||
@@ -2,6 +2,9 @@ import { Component } from 'react';
|
||||
import Box from '@mui/material/Box';
|
||||
import { thumb } from '../../api';
|
||||
import ThumbHashImage from './ThumbHashImage';
|
||||
import {
|
||||
SCRIM_SOFT,
|
||||
} from '../../colors';
|
||||
|
||||
class ArticlePhoto extends Component {
|
||||
constructor(props) {
|
||||
@@ -53,7 +56,7 @@ class ArticlePhoto extends Component {
|
||||
justifyContent: 'center',
|
||||
borderRadius: fill ? 0 : 2,
|
||||
overflow: 'hidden',
|
||||
bgcolor: 'rgba(0, 0, 0, 0.35)',
|
||||
bgcolor: SCRIM_SOFT,
|
||||
...sx,
|
||||
}}
|
||||
>
|
||||
|
||||
@@ -27,22 +27,30 @@ import {
|
||||
} from '../../utils/articleDetail';
|
||||
import { notifyError } from '../../utils/notify';
|
||||
import { readClipboardImage } from '../../utils/pasteImage';
|
||||
import {
|
||||
SCRIM_BUTTON,
|
||||
SCRIM_LABEL,
|
||||
SCRIM_SOFT,
|
||||
SCRIM_STRONG,
|
||||
VEIL_MUTED,
|
||||
WHITE,
|
||||
} from '../../colors';
|
||||
import { prepareArticlePhoto } from '../../utils/prepareArticlePhoto';
|
||||
import ArticlePhoto from './ArticlePhoto';
|
||||
import ArticleImageSearchDialog from './ArticleImageSearchDialog';
|
||||
|
||||
const overlayChipSx = {
|
||||
height: 22,
|
||||
bgcolor: 'rgba(0, 0, 0, 0.65)',
|
||||
color: '#fff',
|
||||
bgcolor: SCRIM_LABEL,
|
||||
color: WHITE,
|
||||
'& .MuiChip-label': { px: 0.75, fontSize: 11 },
|
||||
};
|
||||
|
||||
const overlayIconSx = {
|
||||
color: '#fff',
|
||||
bgcolor: 'rgba(0, 0, 0, 0.55)',
|
||||
'&:hover': { bgcolor: 'rgba(0, 0, 0, 0.75)' },
|
||||
'&.Mui-disabled': { color: 'rgba(255,255,255,0.35)', bgcolor: 'rgba(0,0,0,0.35)' },
|
||||
color: WHITE,
|
||||
bgcolor: SCRIM_BUTTON,
|
||||
'&:hover': { bgcolor: SCRIM_STRONG },
|
||||
'&.Mui-disabled': { color: VEIL_MUTED, bgcolor: SCRIM_SOFT },
|
||||
};
|
||||
|
||||
class ArticlePhotosDialog extends Component {
|
||||
|
||||
@@ -32,6 +32,15 @@ import { formatStock } from './searchFormat';
|
||||
import { notifyError } from '../../utils/notify';
|
||||
import artikelAutomat from '../../automats/artikelAutomat';
|
||||
import warehouseAutomat from '../../automats/warehouseAutomat';
|
||||
import {
|
||||
BORDER,
|
||||
GREEN_BG,
|
||||
GREEN_PANEL,
|
||||
RED_BG,
|
||||
RED_PANEL,
|
||||
SURFACE_BASE,
|
||||
SURFACE_RAISED,
|
||||
} from '../../colors';
|
||||
|
||||
const REASON_PRESETS = [
|
||||
'Inventurdifferenz',
|
||||
@@ -344,8 +353,8 @@ export default class ArticleStockAdjustDialog extends PureComponent {
|
||||
sx={{
|
||||
p: 1.5,
|
||||
borderRadius: 1,
|
||||
backgroundColor: '#f8fafc',
|
||||
border: '1px solid #e2e8f0',
|
||||
backgroundColor: SURFACE_BASE,
|
||||
border: `1px solid ${BORDER}`,
|
||||
}}
|
||||
>
|
||||
<Typography variant="caption" color="text.secondary" display="block">
|
||||
@@ -361,8 +370,8 @@ export default class ArticleStockAdjustDialog extends PureComponent {
|
||||
sx={{
|
||||
p: 1.5,
|
||||
borderRadius: 1,
|
||||
backgroundColor: '#f8fafc',
|
||||
border: '1px solid #e2e8f0',
|
||||
backgroundColor: SURFACE_BASE,
|
||||
border: `1px solid ${BORDER}`,
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
justifyContent: 'space-between',
|
||||
@@ -437,8 +446,8 @@ export default class ArticleStockAdjustDialog extends PureComponent {
|
||||
p: 1,
|
||||
px: 2,
|
||||
borderRadius: 1,
|
||||
backgroundColor: '#f1f5f9',
|
||||
border: '1px solid #e2e8f0',
|
||||
backgroundColor: SURFACE_RAISED,
|
||||
border: `1px solid ${BORDER}`,
|
||||
minWidth: 140,
|
||||
}}
|
||||
>
|
||||
@@ -529,8 +538,8 @@ export default class ArticleStockAdjustDialog extends PureComponent {
|
||||
sx={{
|
||||
p: 1.5,
|
||||
borderRadius: 1,
|
||||
backgroundColor: amount > 0 ? '#ecfdf5' : '#fef2f2',
|
||||
border: `1px solid ${amount > 0 ? '#a7f3d0' : '#fecaca'}`,
|
||||
backgroundColor: amount > 0 ? GREEN_BG : RED_BG,
|
||||
border: `1px solid ${amount > 0 ? GREEN_PANEL : RED_PANEL}`,
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
justifyContent: 'space-between',
|
||||
|
||||
@@ -31,40 +31,50 @@ import ArticleSeoTab from './tabs/ArticleSeoTab';
|
||||
import ArticleSettingsTab from './tabs/ArticleSettingsTab';
|
||||
import ArticleSupplierTab from './tabs/ArticleSupplierTab';
|
||||
import HelpBadge from '../HelpBadge';
|
||||
import {
|
||||
BLUE_PANEL,
|
||||
BORDER,
|
||||
GREEN_PANEL,
|
||||
ORANGE_PANEL,
|
||||
PETROL_PANEL,
|
||||
RED_PANEL,
|
||||
SHADOW_TAB,
|
||||
YELLOW_PANEL,
|
||||
} from '../../colors';
|
||||
|
||||
const ALWAYS_TABS = [
|
||||
{
|
||||
id: ARTICLE_TAB.history,
|
||||
label: 'History',
|
||||
color: '#fed7aa',
|
||||
color: ORANGE_PANEL,
|
||||
Icon: HistoryIcon,
|
||||
help: 'Historie aller Änderungen, Buchungen und Ereignisse zu diesem Artikel',
|
||||
},
|
||||
{
|
||||
id: ARTICLE_TAB.supplier,
|
||||
label: 'Supplier',
|
||||
color: '#bbf7d0',
|
||||
color: GREEN_PANEL,
|
||||
Icon: LocalShippingIcon,
|
||||
help: 'Zugeordnete Lieferanten, Einkaufspreise und Lieferanten-Artikelnummern',
|
||||
},
|
||||
{
|
||||
id: ARTICLE_TAB.seo,
|
||||
label: 'SEO & Beschreibung',
|
||||
color: '#99f6e4',
|
||||
color: PETROL_PANEL,
|
||||
Icon: ManageSearchIcon,
|
||||
help: 'Shop-Texte, Beschreibungen und Suchmaschinen-Metadaten',
|
||||
},
|
||||
{
|
||||
id: ARTICLE_TAB.label,
|
||||
label: 'Label',
|
||||
color: '#fde68a',
|
||||
color: YELLOW_PANEL,
|
||||
Icon: LabelIcon,
|
||||
help: 'Barcode- und Preisetiketten für diesen Artikel gestalten und drucken',
|
||||
},
|
||||
{
|
||||
id: ARTICLE_TAB.attributes,
|
||||
label: 'Attribute und Merkmale',
|
||||
color: '#bfdbfe',
|
||||
color: BLUE_PANEL,
|
||||
Icon: CategoryIcon,
|
||||
help: 'Zusatzattribute, technische Merkmale und Filtereigenschaften',
|
||||
},
|
||||
@@ -73,7 +83,7 @@ const ALWAYS_TABS = [
|
||||
const SETTINGS_TAB = {
|
||||
id: ARTICLE_TAB.settings,
|
||||
label: 'Einstellungen',
|
||||
color: '#fca5a5',
|
||||
color: RED_PANEL,
|
||||
Icon: SettingsIcon,
|
||||
help: 'Artikeleinstellungen, Einheiten, Steuersatz und Löschen',
|
||||
};
|
||||
@@ -220,8 +230,7 @@ class ArticleTileTab extends PureComponent {
|
||||
display: 'flex',
|
||||
flexDirection: 'column',
|
||||
minWidth: 0,
|
||||
filter:
|
||||
'drop-shadow(0px 2px 4px rgba(0,0,0,0.2)) drop-shadow(0px 4px 5px rgba(0,0,0,0.14)) drop-shadow(0px 1px 10px rgba(0,0,0,0.12))',
|
||||
filter: SHADOW_TAB,
|
||||
}}
|
||||
>
|
||||
<Box
|
||||
@@ -313,7 +322,7 @@ class ArticleTileTab extends PureComponent {
|
||||
},
|
||||
'&.Mui-disabled': {
|
||||
opacity: 0.4,
|
||||
bgcolor: '#e2e8f0',
|
||||
bgcolor: BORDER,
|
||||
cursor: 'not-allowed',
|
||||
pointerEvents: 'auto',
|
||||
},
|
||||
|
||||
@@ -16,6 +16,9 @@ import ArticleViewBrand from './boxes/ArticleViewBrand';
|
||||
import ArticleViewEANs from './boxes/ArticleViewEANs';
|
||||
import ArticleViewPrice from './boxes/ArticleViewPrice';
|
||||
import ArticleViewLists from './boxes/ArticleViewLists';
|
||||
import {
|
||||
YELLOW_PANEL,
|
||||
} from '../../colors';
|
||||
|
||||
/** Stable prop so PureComponent photo tile does not see a new sx each render. */
|
||||
const PHOTO_TILE_SX = { minHeight: 0 };
|
||||
@@ -93,7 +96,7 @@ class ArticleView extends PureComponent {
|
||||
|
||||
<ArticlePhotoTile
|
||||
wide={1}
|
||||
color="#fde68a"
|
||||
color={YELLOW_PANEL}
|
||||
sx={PHOTO_TILE_SX}
|
||||
article={article}
|
||||
/>
|
||||
|
||||
@@ -10,6 +10,9 @@ import { requireObject } from '../../../utils/apiShape';
|
||||
import { notifyError } from '../../../utils/notify';
|
||||
import InputAdornment from '@mui/material/InputAdornment';
|
||||
import { BrandLabel, BrandThumb } from '../../brand';
|
||||
import {
|
||||
PURPLE_BG,
|
||||
} from '../../../colors';
|
||||
|
||||
class PanelValue extends Component {
|
||||
render() {
|
||||
@@ -174,7 +177,7 @@ export default class ArticleViewBrand extends PureComponent {
|
||||
title="Marke"
|
||||
wide={1}
|
||||
minHeight={SECOND_ROW_MIN_HEIGHT}
|
||||
color="#e9d5ff"
|
||||
color={PURPLE_BG}
|
||||
contentAlignItems="flex-start"
|
||||
value={value}
|
||||
onSave={this.handleSave}
|
||||
|
||||
@@ -9,6 +9,10 @@ import { requireObject } from '../../../utils/apiShape';
|
||||
import { articleEans } from '../../../utils/articleDetail';
|
||||
import { notifyError } from '../../../utils/notify';
|
||||
import JsBarcode from 'jsbarcode';
|
||||
import {
|
||||
BORDER,
|
||||
INK,
|
||||
} from '../../../colors';
|
||||
class PanelValue extends Component {
|
||||
render() {
|
||||
const { label, children } = this.props;
|
||||
@@ -61,7 +65,7 @@ class SkuBarcode extends PureComponent {
|
||||
displayValue: false,
|
||||
margin: 0,
|
||||
background: 'transparent',
|
||||
lineColor: '#1e293b',
|
||||
lineColor: INK,
|
||||
});
|
||||
} catch (e) {
|
||||
console.warn('Barcode render error:', e);
|
||||
@@ -151,7 +155,7 @@ export default class ArticleViewEANs extends PureComponent {
|
||||
title={articleData.sku}
|
||||
wide={1}
|
||||
minHeight={SECOND_ROW_MIN_HEIGHT}
|
||||
color="#e2e8f0"
|
||||
color={BORDER}
|
||||
contentAlignItems="flex-start"
|
||||
value={value}
|
||||
onSave={this.handleSave}
|
||||
|
||||
@@ -36,6 +36,10 @@ import attributeAutomat, { selectAttributes } from '../../../automats/attributeA
|
||||
import artikelAutomat, { getArticle } from '../../../automats/artikelAutomat';
|
||||
import { notifyError } from '../../../utils/notify';
|
||||
import articleIndexAutomat from '../../../automats/articleIndexAutomat';
|
||||
import {
|
||||
MAGENTA_PANEL,
|
||||
SHADOW_CARD,
|
||||
} from '../../../colors';
|
||||
|
||||
export default class ArticleViewLists extends PureComponent {
|
||||
constructor(props) {
|
||||
@@ -1157,7 +1161,7 @@ export default class ArticleViewLists extends PureComponent {
|
||||
transition: 'all 0.15s ease-in-out',
|
||||
'&:hover': {
|
||||
borderColor: 'primary.main',
|
||||
boxShadow: '0 1px 4px rgba(0,0,0,0.06)',
|
||||
boxShadow: SHADOW_CARD,
|
||||
},
|
||||
}}
|
||||
>
|
||||
@@ -1245,7 +1249,7 @@ export default class ArticleViewLists extends PureComponent {
|
||||
transition: 'all 0.15s ease-in-out',
|
||||
'&:hover': {
|
||||
borderColor: 'primary.main',
|
||||
boxShadow: '0 1px 4px rgba(0,0,0,0.06)',
|
||||
boxShadow: SHADOW_CARD,
|
||||
},
|
||||
}}
|
||||
>
|
||||
@@ -1465,7 +1469,7 @@ export default class ArticleViewLists extends PureComponent {
|
||||
const parentArticle = parentSku ? getArticle(parentSku) : null;
|
||||
|
||||
return (
|
||||
<PanelTile wide={1} minHeight={200} color="#fbcfe8">
|
||||
<PanelTile wide={1} minHeight={200} color={MAGENTA_PANEL}>
|
||||
<Box sx={{ display: 'flex', flexDirection: 'column', height: '100%', gap: 1 }}>
|
||||
{/* ── Mode 1: Parent Article ──────────────────────────────────────── */}
|
||||
{isParent ? (
|
||||
@@ -1640,7 +1644,7 @@ export default class ArticleViewLists extends PureComponent {
|
||||
transition: 'all 0.15s ease-in-out',
|
||||
'&:hover': {
|
||||
borderColor: 'primary.main',
|
||||
boxShadow: '0 1px 4px rgba(0,0,0,0.06)',
|
||||
boxShadow: SHADOW_CARD,
|
||||
},
|
||||
}}
|
||||
>
|
||||
|
||||
@@ -8,6 +8,9 @@ import { Box, Chip, IconButton } from '@mui/material';
|
||||
import AddIcon from '@mui/icons-material/Add';
|
||||
import CategoryPickerDialog from '../CategoryPickerDialog';
|
||||
import { notifyError } from '../../../utils/notify';
|
||||
import {
|
||||
WHITE,
|
||||
} from '../../../colors';
|
||||
|
||||
const articleNameTextSx = {
|
||||
fontWeight: 700,
|
||||
@@ -327,7 +330,7 @@ export default class ArticleViewName extends PureComponent {
|
||||
wide={3}
|
||||
minHeight={0}
|
||||
sx={headerTileSx}
|
||||
color="#ffffff"
|
||||
color={WHITE}
|
||||
value={value}
|
||||
onSave={this.handleSave}
|
||||
onError={notifyError}
|
||||
|
||||
@@ -31,6 +31,10 @@ import { notifyError } from '../../../utils/notify';
|
||||
import { PriceTaxRow } from './PriceTaxRow';
|
||||
import TierPricePills from './TierPricePills';
|
||||
import { PriceUnitRow, PriceUnitCorner } from './PriceUnitRow';
|
||||
import {
|
||||
BLUE_PANEL,
|
||||
BORDER,
|
||||
} from '../../../colors';
|
||||
|
||||
const SECOND_ROW_MIN_HEIGHT = 220;
|
||||
|
||||
@@ -481,7 +485,7 @@ export default class ArticleViewPrice extends PureComponent {
|
||||
<PanelTile
|
||||
wide={1}
|
||||
minHeight={SECOND_ROW_MIN_HEIGHT}
|
||||
color="#e2e8f0"
|
||||
color={BORDER}
|
||||
contentAlignItems="flex-start"
|
||||
>
|
||||
<Box sx={{ display: 'flex', flexDirection: 'column', height: '100%', width: '100%' }}>
|
||||
@@ -533,7 +537,7 @@ export default class ArticleViewPrice extends PureComponent {
|
||||
<EditablePanelTile
|
||||
wide={1}
|
||||
minHeight={SECOND_ROW_MIN_HEIGHT}
|
||||
color="#bfdbfe"
|
||||
color={BLUE_PANEL}
|
||||
contentAlignItems="flex-start"
|
||||
value={priceValue}
|
||||
onSave={this.handleSave}
|
||||
|
||||
@@ -7,6 +7,14 @@ import Tooltip from '@mui/material/Tooltip';
|
||||
import { PanelTile } from 'dashgui/panels';
|
||||
import { formatStock } from '../searchFormat';
|
||||
import ArticleStockAdjustDialog from '../ArticleStockAdjustDialog';
|
||||
import {
|
||||
BORDER,
|
||||
GREEN_PANEL,
|
||||
PETROL_PANEL,
|
||||
SORT_GHOST_BG,
|
||||
TINT_ROW_HOVER,
|
||||
VEIL_PANEL,
|
||||
} from '../../../colors';
|
||||
|
||||
const SECOND_ROW_MIN_HEIGHT = 220;
|
||||
|
||||
@@ -66,7 +74,7 @@ export default class ArticleViewStock extends PureComponent {
|
||||
<PanelTile
|
||||
wide={1}
|
||||
minHeight={SECOND_ROW_MIN_HEIGHT}
|
||||
color={isParent ? '#e2e8f0' : isStueckliste ? '#cffafe' : '#bbf7d0'}
|
||||
color={isParent ? BORDER : isStueckliste ? PETROL_PANEL : GREEN_PANEL}
|
||||
action={
|
||||
isParent || isStueckliste
|
||||
? null
|
||||
@@ -171,7 +179,7 @@ export default class ArticleViewStock extends PureComponent {
|
||||
mx: -0.5,
|
||||
transition: 'background-color 0.15s ease',
|
||||
'&:hover': {
|
||||
backgroundColor: 'rgba(0, 0, 0, 0.05)',
|
||||
backgroundColor: TINT_ROW_HOVER,
|
||||
},
|
||||
}}
|
||||
>
|
||||
@@ -209,7 +217,7 @@ export default class ArticleViewStock extends PureComponent {
|
||||
mx: -0.5,
|
||||
transition: 'background-color 0.15s ease',
|
||||
'&:hover': {
|
||||
backgroundColor: 'rgba(0, 0, 0, 0.05)',
|
||||
backgroundColor: TINT_ROW_HOVER,
|
||||
},
|
||||
}}
|
||||
>
|
||||
@@ -246,7 +254,7 @@ export default class ArticleViewStock extends PureComponent {
|
||||
pr: 0.5,
|
||||
'&::-webkit-scrollbar': { width: 4 },
|
||||
'&::-webkit-scrollbar-thumb': {
|
||||
backgroundColor: 'rgba(0, 0, 0, 0.15)',
|
||||
backgroundColor: SORT_GHOST_BG,
|
||||
borderRadius: 2,
|
||||
},
|
||||
}}
|
||||
@@ -262,7 +270,7 @@ export default class ArticleViewStock extends PureComponent {
|
||||
py: 0.35,
|
||||
px: 0.75,
|
||||
borderRadius: 1,
|
||||
backgroundColor: 'rgba(255, 255, 255, 0.55)',
|
||||
backgroundColor: VEIL_PANEL,
|
||||
}}
|
||||
>
|
||||
<Box
|
||||
|
||||
@@ -3,6 +3,11 @@ import Box from '@mui/material/Box';
|
||||
import Chip from '@mui/material/Chip';
|
||||
import IconButton from '@mui/material/IconButton';
|
||||
import LayersOutlinedIcon from '@mui/icons-material/LayersOutlined';
|
||||
import {
|
||||
TINT_CHIP,
|
||||
TINT_PILL_ACTIVE,
|
||||
TINT_STRONG,
|
||||
} from '../../../colors';
|
||||
|
||||
const taxPillSx = {
|
||||
height: 28,
|
||||
@@ -27,12 +32,12 @@ export class TaxRatePills extends Component {
|
||||
sx={{
|
||||
...taxPillSx,
|
||||
visibility: active || !readOnly ? 'visible' : 'hidden',
|
||||
bgcolor: active ? 'rgba(0, 0, 0, 0.14)' : 'rgba(0, 0, 0, 0.06)',
|
||||
bgcolor: active ? TINT_PILL_ACTIVE : TINT_CHIP,
|
||||
...(readOnly
|
||||
? {}
|
||||
: {
|
||||
cursor: 'pointer',
|
||||
'&:hover': { bgcolor: 'rgba(0, 0, 0, 0.12)' },
|
||||
'&:hover': { bgcolor: TINT_STRONG },
|
||||
}),
|
||||
}}
|
||||
/>
|
||||
|
||||
@@ -10,6 +10,9 @@ import StraightenOutlinedIcon from '@mui/icons-material/StraightenOutlined';
|
||||
import Switch from '@mui/material/Switch';
|
||||
import TextInput from '../../TextInput';
|
||||
import { findUnitBySi, unitLabel } from '../../../utils/units';
|
||||
import {
|
||||
TINT_PILL,
|
||||
} from '../../../colors';
|
||||
|
||||
const priceUnitCornerSx = {
|
||||
position: 'absolute',
|
||||
@@ -49,7 +52,7 @@ export class PriceUnitRow extends Component {
|
||||
height: 28,
|
||||
fontSize: 13,
|
||||
fontWeight: 600,
|
||||
bgcolor: 'rgba(0, 0, 0, 0.08)',
|
||||
bgcolor: TINT_PILL,
|
||||
}}
|
||||
/>
|
||||
) : null}
|
||||
|
||||
@@ -2,6 +2,9 @@ import { Component } from 'react';
|
||||
import Box from '@mui/material/Box';
|
||||
import Chip from '@mui/material/Chip';
|
||||
import { formatPrice, parsePriceInput } from '../../../utils/price';
|
||||
import {
|
||||
TINT_PILL,
|
||||
} from '../../../colors';
|
||||
|
||||
const tierPillSx = {
|
||||
height: 22,
|
||||
@@ -24,7 +27,7 @@ export default class TierPricePills extends Component {
|
||||
label={`ab ${tier.minmultiplier} · ${formatPrice(gross)}`}
|
||||
sx={{
|
||||
...tierPillSx,
|
||||
bgcolor: 'rgba(0, 0, 0, 0.08)',
|
||||
bgcolor: TINT_PILL,
|
||||
}}
|
||||
/>
|
||||
);
|
||||
|
||||
@@ -15,6 +15,21 @@ function formatDate(iso) {
|
||||
return new Date(iso).toLocaleString('de-DE');
|
||||
}
|
||||
|
||||
import {
|
||||
SCRIM_SOFT,
|
||||
} from '../../../colors';
|
||||
|
||||
const HISTORY_DATE_CELL_BORDER = `1px solid ${SCRIM_SOFT}`;
|
||||
|
||||
const dateDividerSx = {
|
||||
borderBottom: HISTORY_DATE_CELL_BORDER,
|
||||
};
|
||||
|
||||
const dateCellSx = {
|
||||
borderBottom: HISTORY_DATE_CELL_BORDER,
|
||||
verticalAlign: 'middle',
|
||||
};
|
||||
|
||||
export default class ArticleHistoryTab extends PureComponent {
|
||||
constructor(props) {
|
||||
super(props);
|
||||
@@ -94,7 +109,7 @@ export default class ArticleHistoryTab extends PureComponent {
|
||||
<TableRow>
|
||||
<TableCell
|
||||
sx={{
|
||||
borderBottom: '1px solid rgba(0, 0, 0, 0.35)',
|
||||
...dateDividerSx,
|
||||
width: '1%',
|
||||
whiteSpace: 'nowrap',
|
||||
verticalAlign: 'middle',
|
||||
@@ -103,10 +118,7 @@ export default class ArticleHistoryTab extends PureComponent {
|
||||
Datum
|
||||
</TableCell>
|
||||
<TableCell
|
||||
sx={{
|
||||
borderBottom: '1px solid rgba(0, 0, 0, 0.35)',
|
||||
verticalAlign: 'middle',
|
||||
}}
|
||||
sx={dateCellSx}
|
||||
>
|
||||
{showPagination ? (
|
||||
<ResultsPagination
|
||||
|
||||
@@ -28,6 +28,10 @@ import { articleSupplierAutomat } from '../../../automats/articleDetailAutomat';
|
||||
import sseAutomat from '../../../automats/sseAutomat';
|
||||
import companyAutomat from '../../../automats/companyAutomat';
|
||||
import ArticleSupplierDialog from '../ArticleSupplierDialog';
|
||||
import {
|
||||
SORT_GHOST_BG,
|
||||
TINT_FAINT,
|
||||
} from '../../../colors';
|
||||
|
||||
export default class ArticleSupplierTab extends PureComponent {
|
||||
constructor(props) {
|
||||
@@ -218,8 +222,8 @@ export default class ArticleSupplierTab extends PureComponent {
|
||||
py: 6,
|
||||
px: 2,
|
||||
borderRadius: 2,
|
||||
border: '1px dashed rgba(0, 0, 0, 0.15)',
|
||||
bgcolor: 'rgba(0, 0, 0, 0.01)',
|
||||
border: `1px dashed ${SORT_GHOST_BG}`,
|
||||
bgcolor: TINT_FAINT,
|
||||
textAlign: 'center',
|
||||
}}
|
||||
>
|
||||
@@ -286,8 +290,8 @@ export default class ArticleSupplierTab extends PureComponent {
|
||||
py: 6,
|
||||
px: 2,
|
||||
borderRadius: 2,
|
||||
border: '1px dashed rgba(0, 0, 0, 0.15)',
|
||||
bgcolor: 'rgba(0, 0, 0, 0.01)',
|
||||
border: `1px dashed ${SORT_GHOST_BG}`,
|
||||
bgcolor: TINT_FAINT,
|
||||
textAlign: 'center',
|
||||
}}
|
||||
>
|
||||
|
||||
@@ -27,6 +27,14 @@ import { enqueueSnackbar } from 'notistack';
|
||||
import { serper } from '../../api';
|
||||
import brandAutomat from '../../automats/brandAutomat';
|
||||
import { downloadAndPrepareLogoAvif } from '../../utils/cropLogo';
|
||||
import {
|
||||
CHECKERBOARD,
|
||||
INFO_RIBBON,
|
||||
SCRIM_LABEL,
|
||||
VEIL_CARD_STRONG,
|
||||
VEIL_OVERLAY,
|
||||
WHITE,
|
||||
} from '../../colors';
|
||||
|
||||
/**
|
||||
* Dialog that searches Google Images via Serper.dev SSE stream for a brand logo.
|
||||
@@ -420,9 +428,8 @@ export default class BrandImageSearchDialog extends PureComponent {
|
||||
overflow: 'hidden',
|
||||
border: '1px solid',
|
||||
borderColor: isThisProcessing ? 'primary.main' : 'divider',
|
||||
bgcolor: '#fff',
|
||||
backgroundImage:
|
||||
'linear-gradient(45deg, #f0f0f0 25%, transparent 25%), linear-gradient(-45deg, #f0f0f0 25%, transparent 25%), linear-gradient(45deg, transparent 75%, #f0f0f0 75%), linear-gradient(-45deg, transparent 75%, #f0f0f0 75%)',
|
||||
bgcolor: WHITE,
|
||||
backgroundImage: CHECKERBOARD,
|
||||
backgroundSize: '16px 16px',
|
||||
backgroundPosition: '0 0, 0 8px, 8px -8px, -8px 0px',
|
||||
cursor: processingId ? 'default' : 'pointer',
|
||||
@@ -484,8 +491,8 @@ export default class BrandImageSearchDialog extends PureComponent {
|
||||
height: 16,
|
||||
fontSize: '0.62rem',
|
||||
fontWeight: 600,
|
||||
bgcolor: 'rgba(0, 0, 0, 0.65)',
|
||||
color: '#fff',
|
||||
bgcolor: SCRIM_LABEL,
|
||||
color: WHITE,
|
||||
pointerEvents: 'none',
|
||||
px: 0.25,
|
||||
}}
|
||||
@@ -503,9 +510,9 @@ export default class BrandImageSearchDialog extends PureComponent {
|
||||
right: 0,
|
||||
py: 0.6,
|
||||
px: 0.8,
|
||||
bgcolor: 'rgba(25, 118, 210, 0.92)',
|
||||
bgcolor: INFO_RIBBON,
|
||||
backdropFilter: 'blur(3px)',
|
||||
color: '#fff',
|
||||
color: WHITE,
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
justifyContent: 'center',
|
||||
@@ -532,7 +539,7 @@ export default class BrandImageSearchDialog extends PureComponent {
|
||||
sx={{
|
||||
position: 'absolute',
|
||||
inset: 0,
|
||||
bgcolor: 'rgba(255, 255, 255, 0.92)',
|
||||
bgcolor: VEIL_OVERLAY,
|
||||
backdropFilter: 'blur(3px)',
|
||||
display: 'flex',
|
||||
flexDirection: 'column',
|
||||
@@ -578,9 +585,9 @@ export default class BrandImageSearchDialog extends PureComponent {
|
||||
onClick={(e) => this.handleCopyUrl(img.url, e)}
|
||||
sx={{
|
||||
p: 0.5,
|
||||
bgcolor: 'rgba(255, 255, 255, 0.9)',
|
||||
bgcolor: VEIL_CARD_STRONG,
|
||||
boxShadow: 1,
|
||||
'&:hover': { bgcolor: '#fff' },
|
||||
'&:hover': { bgcolor: WHITE },
|
||||
}}
|
||||
>
|
||||
<ContentCopyIcon sx={{ fontSize: 14, color: 'text.primary' }} />
|
||||
@@ -592,9 +599,9 @@ export default class BrandImageSearchDialog extends PureComponent {
|
||||
onClick={(e) => this.handleOpenUrl(img.url, e)}
|
||||
sx={{
|
||||
p: 0.5,
|
||||
bgcolor: 'rgba(255, 255, 255, 0.9)',
|
||||
bgcolor: VEIL_CARD_STRONG,
|
||||
boxShadow: 1,
|
||||
'&:hover': { bgcolor: '#fff' },
|
||||
'&:hover': { bgcolor: WHITE },
|
||||
}}
|
||||
>
|
||||
<OpenInNewIcon sx={{ fontSize: 14, color: 'text.primary' }} />
|
||||
|
||||
@@ -1,6 +1,10 @@
|
||||
import Box from '@mui/material/Box';
|
||||
import ThumbHashImage, { getThumbHashDimensions } from '../article/ThumbHashImage';
|
||||
import brandAutomat from '../../automats/brandAutomat';
|
||||
import {
|
||||
SHADOW_THUMB_RING,
|
||||
TINT_ROW_HOVER,
|
||||
} from '../../colors';
|
||||
|
||||
/**
|
||||
* Extracts thumbHash from various brand representations:
|
||||
@@ -97,8 +101,8 @@ export default function BrandThumb({
|
||||
overflow: 'hidden',
|
||||
flexShrink: 0,
|
||||
verticalAlign: 'middle',
|
||||
bgcolor: 'rgba(0, 0, 0, 0.05)',
|
||||
boxShadow: '0 0 0 1px rgba(0, 0, 0, 0.12)',
|
||||
bgcolor: TINT_ROW_HOVER,
|
||||
boxShadow: SHADOW_THUMB_RING,
|
||||
...sx,
|
||||
}}
|
||||
>
|
||||
|
||||
@@ -2,9 +2,14 @@ import React, { useState } from 'react';
|
||||
import Box from '@mui/material/Box';
|
||||
import Typography from '@mui/material/Typography';
|
||||
import Button from '@mui/material/Button';
|
||||
import TextField from '@mui/material/TextField';
|
||||
import TextInput from '../TextInput';
|
||||
import HelpOutlineRoundedIcon from '@mui/icons-material/HelpOutlineRounded';
|
||||
import SendRoundedIcon from '@mui/icons-material/SendRounded';
|
||||
import {
|
||||
ASK_PROMPT_BG_PAIR,
|
||||
ASK_PROMPT_BORDER_PAIR,
|
||||
byMode,
|
||||
} from '../../colors';
|
||||
|
||||
export default function AskUserPrompt({ askData, onAnswer, disabled = false }) {
|
||||
const { toolCallId, question, options = [] } = askData;
|
||||
@@ -30,8 +35,8 @@ export default function AskUserPrompt({ askData, onAnswer, disabled = false }) {
|
||||
my: 1,
|
||||
p: 2,
|
||||
borderRadius: 3,
|
||||
bgcolor: theme.palette.mode === 'dark' ? 'rgba(59, 91, 219, 0.12)' : '#eef2ff',
|
||||
border: `1px solid ${theme.palette.mode === 'dark' ? 'rgba(59, 91, 219, 0.3)' : '#c7d2fe'}`,
|
||||
bgcolor: byMode(theme, ASK_PROMPT_BG_PAIR),
|
||||
border: `1px solid ${byMode(theme, ASK_PROMPT_BORDER_PAIR)}`,
|
||||
display: 'flex',
|
||||
flexDirection: 'column',
|
||||
gap: 1.5,
|
||||
@@ -97,28 +102,13 @@ export default function AskUserPrompt({ askData, onAnswer, disabled = false }) {
|
||||
onSubmit={handleCustomSubmit}
|
||||
sx={{ display: 'flex', gap: 1, mt: 0.5, alignItems: 'center' }}
|
||||
>
|
||||
<TextField
|
||||
size="small"
|
||||
<TextInput
|
||||
fullWidth
|
||||
autoFocus
|
||||
placeholder="Antwort eingeben…"
|
||||
value={customAnswer}
|
||||
onChange={(e) => setCustomAnswer(e.target.value)}
|
||||
disabled={disabled}
|
||||
slotProps={{
|
||||
htmlInput: {
|
||||
spellCheck: false,
|
||||
autoCorrect: 'off',
|
||||
autoCapitalize: 'off',
|
||||
},
|
||||
}}
|
||||
sx={{
|
||||
'& .MuiOutlinedInput-root': {
|
||||
borderRadius: 2,
|
||||
bgcolor: 'background.paper',
|
||||
fontSize: '0.82rem',
|
||||
},
|
||||
}}
|
||||
/>
|
||||
<Button
|
||||
type="submit"
|
||||
|
||||
@@ -4,7 +4,7 @@ import Box from '@mui/material/Box';
|
||||
import Typography from '@mui/material/Typography';
|
||||
import IconButton from '@mui/material/IconButton';
|
||||
import Button from '@mui/material/Button';
|
||||
import TextField from '@mui/material/TextField';
|
||||
import TextInput from '../TextInput';
|
||||
import MenuItem from '@mui/material/MenuItem';
|
||||
import Select from '@mui/material/Select';
|
||||
import FormControl from '@mui/material/FormControl';
|
||||
@@ -20,6 +20,10 @@ import chatAutomat, { selectPendingAskUser } from '../../automats/chatAutomat';
|
||||
import userSettingsAutomat from '../../automats/userSettingsAutomat';
|
||||
import ChatFlowList from './ChatFlowList';
|
||||
import AskUserPrompt from './AskUserPrompt';
|
||||
import {
|
||||
BLUE_GLOW,
|
||||
SHADOW_DRAWER,
|
||||
} from '../../colors';
|
||||
|
||||
const DEFAULT_DRAWER_WIDTH = 460;
|
||||
const MIN_DRAWER_WIDTH = 320;
|
||||
@@ -489,11 +493,10 @@ export default class ChatDrawer extends PureComponent {
|
||||
)}
|
||||
|
||||
<Box sx={{ display: 'flex', alignItems: 'flex-end', gap: 1 }}>
|
||||
<TextField
|
||||
<TextInput
|
||||
inputRef={this.inputRef}
|
||||
multiline
|
||||
maxRows={4}
|
||||
size="small"
|
||||
fullWidth
|
||||
value={inputText}
|
||||
onChange={(e) => this.setState({ inputText: e.target.value })}
|
||||
@@ -502,22 +505,8 @@ export default class ChatDrawer extends PureComponent {
|
||||
placeholder={
|
||||
pendingAsk
|
||||
? 'Bitte Frage oben beantworten…'
|
||||
: 'Nachricht eingeben… (Enter)'
|
||||
: 'Nachricht eingeben…'
|
||||
}
|
||||
slotProps={{
|
||||
htmlInput: {
|
||||
spellCheck: false,
|
||||
autoCorrect: 'off',
|
||||
autoCapitalize: 'off',
|
||||
},
|
||||
}}
|
||||
sx={{
|
||||
'& .MuiOutlinedInput-root': {
|
||||
borderRadius: 2.5,
|
||||
bgcolor: 'background.default',
|
||||
fontSize: '0.875rem',
|
||||
},
|
||||
}}
|
||||
/>
|
||||
|
||||
<IconButton
|
||||
@@ -576,7 +565,7 @@ export default class ChatDrawer extends PureComponent {
|
||||
maxWidth: '100vw',
|
||||
display: 'flex',
|
||||
flexDirection: 'column',
|
||||
boxShadow: '-8px 0 32px rgba(0, 0, 0, 0.12)',
|
||||
boxShadow: SHADOW_DRAWER,
|
||||
position: 'fixed',
|
||||
overflow: 'visible',
|
||||
transition: isResizing ? 'none !important' : undefined,
|
||||
@@ -592,7 +581,7 @@ export default class ChatDrawer extends PureComponent {
|
||||
maxWidth: '100vw',
|
||||
display: 'flex',
|
||||
flexDirection: 'column',
|
||||
boxShadow: '-8px 0 32px rgba(0, 0, 0, 0.12)',
|
||||
boxShadow: SHADOW_DRAWER,
|
||||
position: 'fixed',
|
||||
overflow: 'visible',
|
||||
transition: isResizing ? 'none !important' : undefined,
|
||||
@@ -625,7 +614,7 @@ export default class ChatDrawer extends PureComponent {
|
||||
height: 56,
|
||||
width: 4,
|
||||
opacity: 1,
|
||||
boxShadow: '0 0 10px rgba(59, 91, 219, 0.6)',
|
||||
boxShadow: `0 0 10px ${BLUE_GLOW}`,
|
||||
},
|
||||
}}
|
||||
>
|
||||
@@ -638,7 +627,7 @@ export default class ChatDrawer extends PureComponent {
|
||||
bgcolor: isResizing ? 'primary.main' : 'divider',
|
||||
opacity: isResizing ? 1 : 0.6,
|
||||
transition: 'background-color 0.15s, height 0.15s, width 0.15s, opacity 0.15s, box-shadow 0.15s',
|
||||
boxShadow: isResizing ? '0 0 10px rgba(59, 91, 219, 0.6)' : 'none',
|
||||
boxShadow: isResizing ? `0 0 10px ${BLUE_GLOW}` : 'none',
|
||||
}}
|
||||
/>
|
||||
</Box>
|
||||
|
||||
@@ -10,6 +10,15 @@ import AutoAwesomeRoundedIcon from '@mui/icons-material/AutoAwesomeRounded';
|
||||
import HelpOutlineRoundedIcon from '@mui/icons-material/HelpOutlineRounded';
|
||||
|
||||
import chatAutomat, { selectPendingAskUser, selectHasPendingAsk } from '../../automats/chatAutomat';
|
||||
import {
|
||||
CHAT_BUSY,
|
||||
CHAT_FAB_BUSY,
|
||||
CHAT_FAB_PENDING, CHAT_FAB_SHADOW_BUSY,
|
||||
CHAT_FAB_SHADOW_IDLE,
|
||||
CHAT_FAB_SHADOW_PENDING, RED,
|
||||
RED_GLOW,
|
||||
WHITE,
|
||||
} from '../../colors';
|
||||
|
||||
export default class ChatFab extends PureComponent {
|
||||
constructor(props) {
|
||||
@@ -86,7 +95,7 @@ export default class ChatFab extends PureComponent {
|
||||
right: -8,
|
||||
bottom: -8,
|
||||
borderRadius: '50%',
|
||||
bgcolor: '#f59e0b',
|
||||
bgcolor: CHAT_BUSY,
|
||||
opacity: 0.5,
|
||||
animation: 'fabAskPulse 1.5s cubic-bezier(0.4, 0, 0.6, 1) infinite',
|
||||
'@keyframes fabAskPulse': {
|
||||
@@ -129,7 +138,7 @@ export default class ChatFab extends PureComponent {
|
||||
position: 'absolute',
|
||||
top: -6,
|
||||
left: -6,
|
||||
color: '#f59e0b',
|
||||
color: CHAT_BUSY,
|
||||
pointerEvents: 'none',
|
||||
}}
|
||||
/>
|
||||
@@ -146,14 +155,14 @@ export default class ChatFab extends PureComponent {
|
||||
sx={{
|
||||
'& .MuiBadge-badge': hasPendingAsk
|
||||
? {
|
||||
bgcolor: '#ef4444',
|
||||
color: '#ffffff',
|
||||
bgcolor: RED,
|
||||
color: WHITE,
|
||||
fontWeight: 900,
|
||||
fontSize: '0.85rem',
|
||||
minWidth: 22,
|
||||
height: 22,
|
||||
borderRadius: '50%',
|
||||
boxShadow: '0 2px 10px rgba(239, 68, 68, 0.6)',
|
||||
boxShadow: `0 2px 10px ${RED_GLOW}`,
|
||||
animation: 'badgePop 1.5s ease infinite',
|
||||
'@keyframes badgePop': {
|
||||
'0%, 100%': { transform: 'scale(1)' },
|
||||
@@ -171,10 +180,10 @@ export default class ChatFab extends PureComponent {
|
||||
width: 54,
|
||||
height: 54,
|
||||
boxShadow: hasPendingAsk
|
||||
? '0 8px 24px rgba(245, 158, 11, 0.45)'
|
||||
? CHAT_FAB_SHADOW_PENDING
|
||||
: isBusy
|
||||
? '0 8px 24px rgba(245, 158, 11, 0.35)'
|
||||
: '0 8px 24px rgba(59, 91, 219, 0.35)',
|
||||
? CHAT_FAB_SHADOW_BUSY
|
||||
: CHAT_FAB_SHADOW_IDLE,
|
||||
transition: 'transform 0.2s cubic-bezier(0.34, 1.56, 0.64, 1), background 0.2s',
|
||||
'&:hover': {
|
||||
transform: 'scale(1.06)',
|
||||
@@ -183,9 +192,9 @@ export default class ChatFab extends PureComponent {
|
||||
transform: 'scale(0.96)',
|
||||
},
|
||||
background: hasPendingAsk
|
||||
? 'linear-gradient(135deg, #f59e0b 0%, #ea580c 100%)'
|
||||
? CHAT_FAB_PENDING
|
||||
: isBusy
|
||||
? 'linear-gradient(135deg, #f59e0b 0%, #d97706 100%)'
|
||||
? CHAT_FAB_BUSY
|
||||
: `linear-gradient(135deg, ${theme.palette.primary.main} 0%, ${theme.palette.primary.dark} 100%)`,
|
||||
})}
|
||||
>
|
||||
|
||||
@@ -2,6 +2,13 @@ import React, { PureComponent, createRef } from 'react';
|
||||
import Box from '@mui/material/Box';
|
||||
import Typography from '@mui/material/Typography';
|
||||
import HealingMarkdown from './HealingMarkdown';
|
||||
import {
|
||||
SHADOW_BUBBLE_ASSISTANT,
|
||||
SHADOW_BUBBLE_USER,
|
||||
SURFACE_RAISED,
|
||||
SURFACE_SUBTLE_PAIR,
|
||||
byMode,
|
||||
} from '../../colors';
|
||||
|
||||
function formatMessageTime(isoString) {
|
||||
if (!isoString) return '';
|
||||
@@ -109,7 +116,7 @@ function MessageRow({ msg, showTime = true }) {
|
||||
<Box sx={{ display: 'flex', justifyContent: 'flex-start', py: 0.35, px: 2 }}>
|
||||
<Box
|
||||
sx={(theme) => ({
|
||||
bgcolor: theme.palette.mode === 'dark' ? 'rgba(255,255,255,0.04)' : 'rgba(0,0,0,0.03)',
|
||||
bgcolor: byMode(theme, SURFACE_SUBTLE_PAIR),
|
||||
color: 'text.secondary',
|
||||
px: 1.25,
|
||||
py: 0.35,
|
||||
@@ -193,7 +200,7 @@ function MessageRow({ msg, showTime = true }) {
|
||||
<Box sx={{ display: 'flex', justifyContent: 'flex-start', py: 0.35, px: 2 }}>
|
||||
<Box
|
||||
sx={(theme) => ({
|
||||
bgcolor: theme.palette.mode === 'dark' ? 'rgba(255,255,255,0.04)' : 'rgba(0,0,0,0.03)',
|
||||
bgcolor: byMode(theme, SURFACE_SUBTLE_PAIR),
|
||||
color: 'text.secondary',
|
||||
px: 1.25,
|
||||
py: 0.35,
|
||||
@@ -272,13 +279,13 @@ function MessageRow({ msg, showTime = true }) {
|
||||
? theme.palette.primary.main
|
||||
: theme.palette.mode === 'dark'
|
||||
? 'background.paper'
|
||||
: '#f4f6f8',
|
||||
: SURFACE_RAISED,
|
||||
color: isUser
|
||||
? theme.palette.primary.contrastText
|
||||
: theme.palette.text.primary,
|
||||
boxShadow: isUser
|
||||
? '0 2px 8px rgba(59, 91, 219, 0.25)'
|
||||
: '0 1px 4px rgba(0, 0, 0, 0.05)',
|
||||
? SHADOW_BUBBLE_USER
|
||||
: SHADOW_BUBBLE_ASSISTANT,
|
||||
border: isUser ? 'none' : `1px solid ${theme.palette.divider}`,
|
||||
wordBreak: 'break-word',
|
||||
whiteSpace: isUser ? 'pre-wrap' : 'normal',
|
||||
|
||||
@@ -1,5 +1,12 @@
|
||||
import React, { memo, useState } from 'react';
|
||||
import Box from '@mui/material/Box';
|
||||
import {
|
||||
CODE_BLOCK_PAIR,
|
||||
CODE_INLINE_PAIR,
|
||||
SURFACE_SUBTLE_PAIR,
|
||||
TABLE_HEADER_PAIR,
|
||||
byMode,
|
||||
} from '../../colors';
|
||||
|
||||
/**
|
||||
* Healing markdown renderer.
|
||||
@@ -336,7 +343,7 @@ const rootSx = (theme) => ({
|
||||
fontSize: '0.82em',
|
||||
px: 0.5,
|
||||
borderRadius: 0.75,
|
||||
bgcolor: theme.palette.mode === 'dark' ? 'rgba(255,255,255,0.08)' : 'rgba(0,0,0,0.06)',
|
||||
bbox: byMode(theme, CODE_INLINE_PAIR),
|
||||
},
|
||||
'& pre': {
|
||||
m: 0,
|
||||
@@ -344,7 +351,7 @@ const rootSx = (theme) => ({
|
||||
p: 1.25,
|
||||
borderRadius: 1.5,
|
||||
overflowX: 'auto',
|
||||
bgcolor: theme.palette.mode === 'dark' ? 'rgba(0,0,0,0.35)' : 'rgba(0,0,0,0.06)',
|
||||
bgcolor: byMode(theme, CODE_BLOCK_PAIR),
|
||||
},
|
||||
'& pre code': { p: 0, bgcolor: 'transparent', whiteSpace: 'pre' },
|
||||
'& hr': { border: 0, borderTop: `1px solid ${theme.palette.divider}`, my: 1.5 },
|
||||
@@ -357,7 +364,7 @@ const rootSx = (theme) => ({
|
||||
objectFit: 'contain',
|
||||
borderRadius: 1.5,
|
||||
border: `1px solid ${theme.palette.divider}`,
|
||||
bgcolor: theme.palette.mode === 'dark' ? 'rgba(255,255,255,0.05)' : 'rgba(0,0,0,0.03)',
|
||||
bbox: byMode(theme, SURFACE_SUBTLE_PAIR),
|
||||
},
|
||||
'& .md-image-fallback': {
|
||||
display: 'inline-block',
|
||||
@@ -388,7 +395,7 @@ const rootSx = (theme) => ({
|
||||
'&:last-child': { borderRight: 'none' },
|
||||
},
|
||||
'& th': {
|
||||
bgcolor: theme.palette.mode === 'dark' ? 'rgba(255, 255, 255, 0.05)' : 'rgba(0, 0, 0, 0.04)',
|
||||
bbox: byMode(theme, TABLE_HEADER_PAIR),
|
||||
fontWeight: 600,
|
||||
},
|
||||
'& tr:last-child td': {
|
||||
|
||||
@@ -4,7 +4,7 @@ import DialogTitle from '@mui/material/DialogTitle';
|
||||
import DialogContent from '@mui/material/DialogContent';
|
||||
import DialogActions from '@mui/material/DialogActions';
|
||||
import Button from '@mui/material/Button';
|
||||
import TextField from '@mui/material/TextField';
|
||||
import TextInput from '../TextInput';
|
||||
import MenuItem from '@mui/material/MenuItem';
|
||||
import Box from '@mui/material/Box';
|
||||
import Typography from '@mui/material/Typography';
|
||||
@@ -12,6 +12,9 @@ import CircularProgress from '@mui/material/CircularProgress';
|
||||
import AutoAwesomeRoundedIcon from '@mui/icons-material/AutoAwesomeRounded';
|
||||
|
||||
import { PRESET_MODELS } from '../../automats/chatAutomat';
|
||||
import {
|
||||
SHADOW_DIALOG,
|
||||
} from '../../colors';
|
||||
|
||||
export default class NewConversationDialog extends PureComponent {
|
||||
constructor(props) {
|
||||
@@ -73,7 +76,7 @@ export default class NewConversationDialog extends PureComponent {
|
||||
sx: {
|
||||
borderRadius: 3,
|
||||
p: 1,
|
||||
boxShadow: '0 20px 40px rgba(0,0,0,0.2)',
|
||||
boxShadow: SHADOW_DIALOG,
|
||||
},
|
||||
}}
|
||||
>
|
||||
@@ -103,23 +106,21 @@ export default class NewConversationDialog extends PureComponent {
|
||||
Wähle das Sprachmodell für die neue Konversation aus.
|
||||
</Typography>
|
||||
|
||||
<TextField
|
||||
<TextInput
|
||||
label="Provider"
|
||||
value={provider}
|
||||
onChange={(e) => this.setState({ provider: e.target.value })}
|
||||
fullWidth
|
||||
size="small"
|
||||
disabled={loading}
|
||||
helperText="Standard: openrouter"
|
||||
/>
|
||||
|
||||
<TextField
|
||||
<TextInput
|
||||
select
|
||||
label="Modell Vorlage"
|
||||
value={isCustom ? '__custom__' : model}
|
||||
onChange={this.handleModelChange}
|
||||
fullWidth
|
||||
size="small"
|
||||
disabled={loading}
|
||||
>
|
||||
{PRESET_MODELS.map((item) => (
|
||||
@@ -128,16 +129,15 @@ export default class NewConversationDialog extends PureComponent {
|
||||
</MenuItem>
|
||||
))}
|
||||
<MenuItem value="__custom__">Anderes Modell eingeben…</MenuItem>
|
||||
</TextField>
|
||||
</TextInput>
|
||||
|
||||
{isCustom && (
|
||||
<TextField
|
||||
<TextInput
|
||||
label="Modell-Bezeichnung"
|
||||
placeholder="z. B. anthropic/claude-3.5-haiku"
|
||||
value={customModel}
|
||||
onChange={(e) => this.setState({ customModel: e.target.value })}
|
||||
fullWidth
|
||||
size="small"
|
||||
autoFocus
|
||||
disabled={loading}
|
||||
/>
|
||||
|
||||
@@ -23,6 +23,9 @@ import LinkIcon from '@mui/icons-material/Link';
|
||||
import articleIndexAutomat from '../../automats/articleIndexAutomat';
|
||||
import ArticlePhoto from '../article/ArticlePhoto';
|
||||
import { BrandLabel } from '../brand';
|
||||
import {
|
||||
BORDER,
|
||||
} from '../../colors';
|
||||
|
||||
export default class ArticleSearchDialog extends PureComponent {
|
||||
constructor(props) {
|
||||
@@ -250,7 +253,7 @@ export default class ArticleSearchDialog extends PureComponent {
|
||||
height: 34,
|
||||
borderRadius: 0.75,
|
||||
overflow: 'hidden',
|
||||
bgcolor: '#e2e8f0',
|
||||
bgcolor: BORDER,
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
justifyContent: 'center',
|
||||
|
||||
@@ -10,6 +10,18 @@ import DeleteOutlineOutlinedIcon from '@mui/icons-material/DeleteOutlineOutlined
|
||||
import { tileBaseSx, TILE_SIZE } from 'dashgui/tiles';
|
||||
import { generateTextThumbnail } from '../../utils/textThumbnail';
|
||||
import { notifyErrorMessage } from '../../utils/notify';
|
||||
import {
|
||||
SCRIM_ACTION,
|
||||
SCRIM_ATTACHMENT,
|
||||
SCRIM_HEAVY,
|
||||
SURFACE_BASE,
|
||||
SURFACE_FAINT,
|
||||
SURFACE_RAISED,
|
||||
SURFACE_SUBTLE,
|
||||
VEIL_DISABLED,
|
||||
VEIL_TEXT,
|
||||
WHITE,
|
||||
} from '../../colors';
|
||||
|
||||
export class Attachment extends PureComponent {
|
||||
state = {
|
||||
@@ -120,7 +132,7 @@ export class Attachment extends PureComponent {
|
||||
position: 'relative',
|
||||
overflow: 'hidden',
|
||||
cursor: disabled ? 'default' : 'pointer',
|
||||
bgcolor: isText ? '#fcfcfc' : '#f8fafc',
|
||||
bgcolor: isText ? SURFACE_FAINT : SURFACE_BASE,
|
||||
border: '1px solid',
|
||||
borderColor: 'divider',
|
||||
...(disabled ? { opacity: 0.6, pointerEvents: 'none' } : {}),
|
||||
@@ -135,7 +147,7 @@ export class Attachment extends PureComponent {
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
justifyContent: 'center',
|
||||
bgcolor: isText ? '#fafafa' : '#f1f5f9',
|
||||
bgcolor: isText ? SURFACE_SUBTLE : SURFACE_RAISED,
|
||||
}}
|
||||
>
|
||||
{thumbnail ? (
|
||||
@@ -156,7 +168,7 @@ export class Attachment extends PureComponent {
|
||||
width: '100%',
|
||||
height: '100%',
|
||||
p: 1.25,
|
||||
bgcolor: '#fafafa',
|
||||
bgcolor: SURFACE_SUBTLE,
|
||||
display: 'flex',
|
||||
flexDirection: 'column',
|
||||
justifyContent: 'flex-start',
|
||||
@@ -243,13 +255,13 @@ export class Attachment extends PureComponent {
|
||||
disabled={disabled || hasArticles}
|
||||
onClick={this.handleRescan}
|
||||
sx={{
|
||||
bgcolor: 'rgba(0, 0, 0, 0.6)',
|
||||
color: hasArticles ? 'rgba(255, 255, 255, 0.3)' : '#fff',
|
||||
bgcolor: SCRIM_ACTION,
|
||||
color: hasArticles ? VEIL_DISABLED : WHITE,
|
||||
p: 0.5,
|
||||
backdropFilter: 'blur(2px)',
|
||||
cursor: hasArticles ? 'not-allowed' : 'pointer',
|
||||
'&:hover': {
|
||||
bgcolor: hasArticles ? 'rgba(0, 0, 0, 0.6)' : 'rgba(0, 0, 0, 0.85)',
|
||||
bgcolor: hasArticles ? SCRIM_ACTION : SCRIM_HEAVY,
|
||||
},
|
||||
}}
|
||||
>
|
||||
@@ -265,8 +277,8 @@ export class Attachment extends PureComponent {
|
||||
disabled={disabled}
|
||||
onClick={this.handleDelete}
|
||||
sx={{
|
||||
bgcolor: 'rgba(0, 0, 0, 0.6)',
|
||||
color: '#fff',
|
||||
bgcolor: SCRIM_ACTION,
|
||||
color: WHITE,
|
||||
p: 0.5,
|
||||
backdropFilter: 'blur(2px)',
|
||||
'&:hover': { bgcolor: 'error.main' },
|
||||
@@ -287,7 +299,7 @@ export class Attachment extends PureComponent {
|
||||
bottom: 0,
|
||||
px: 1,
|
||||
py: 0.5,
|
||||
background: 'linear-gradient(transparent, rgba(0,0,0,0.75) 40%, rgba(0,0,0,0.85))',
|
||||
background: SCRIM_ATTACHMENT,
|
||||
textAlign: 'left',
|
||||
pointerEvents: 'none',
|
||||
}}
|
||||
@@ -296,7 +308,7 @@ export class Attachment extends PureComponent {
|
||||
variant="caption"
|
||||
fontWeight={600}
|
||||
noWrap
|
||||
sx={{ color: '#fff', display: 'block', fontSize: '0.72rem', lineHeight: 1.2 }}
|
||||
sx={{ color: WHITE, display: 'block', fontSize: '0.72rem', lineHeight: 1.2 }}
|
||||
>
|
||||
{name}
|
||||
</Typography>
|
||||
@@ -304,7 +316,7 @@ export class Attachment extends PureComponent {
|
||||
<Typography
|
||||
variant="caption"
|
||||
noWrap
|
||||
sx={{ color: 'rgba(255,255,255,0.8)', fontSize: '0.65rem' }}
|
||||
sx={{ color: VEIL_TEXT, fontSize: '0.65rem' }}
|
||||
>
|
||||
{this.formatFileSize(size)}
|
||||
</Typography>
|
||||
|
||||
@@ -35,6 +35,10 @@ import {
|
||||
bookDeliveryLines,
|
||||
} from '../../utils/deliveryLedger';
|
||||
import warehouseAutomat from '../../automats/warehouseAutomat';
|
||||
import {
|
||||
SURFACE_BASE,
|
||||
SURFACE_RAISED,
|
||||
} from '../../colors';
|
||||
|
||||
export default class BookDeliveryDialog extends PureComponent {
|
||||
constructor(props) {
|
||||
@@ -292,7 +296,7 @@ export default class BookDeliveryDialog extends PureComponent {
|
||||
display: 'flex',
|
||||
flexWrap: 'wrap',
|
||||
gap: 3,
|
||||
backgroundColor: '#f8fafc',
|
||||
backgroundColor: SURFACE_BASE,
|
||||
alignItems: 'center',
|
||||
}}
|
||||
>
|
||||
@@ -391,7 +395,7 @@ export default class BookDeliveryDialog extends PureComponent {
|
||||
return (
|
||||
<TableContainer component={Paper} variant="outlined" sx={{ mb: 2 }}>
|
||||
<Table size="small">
|
||||
<TableHead sx={{ backgroundColor: '#f1f5f9' }}>
|
||||
<TableHead sx={{ backgroundColor: SURFACE_RAISED }}>
|
||||
<TableRow>
|
||||
<TableCell sx={{ fontWeight: 600 }}>SKU</TableCell>
|
||||
<TableCell sx={{ fontWeight: 600 }}>Artikel</TableCell>
|
||||
|
||||
@@ -21,6 +21,9 @@ import { notifyError } from '../../utils/notify';
|
||||
import BookDeliveryDialog from './BookDeliveryDialog';
|
||||
import withNavigate from '../withNavigate';
|
||||
import { formatDate, numberDisplay } from './manifestConfig';
|
||||
import {
|
||||
ORANGE_PANEL,
|
||||
} from '../../colors';
|
||||
|
||||
function sortDeliveries(docs) {
|
||||
if (!Array.isArray(docs)) return [];
|
||||
@@ -385,7 +388,7 @@ export class DeliveryList extends PureComponent {
|
||||
onDelete={this.handleDelete}
|
||||
deleteDisabled={busyId != null}
|
||||
emptyMessage="Noch keine Lieferungen."
|
||||
color="#fed7aa"
|
||||
color={ORANGE_PANEL}
|
||||
getEditLabel={(row) => `„${row.number}“ bearbeiten`}
|
||||
getDeleteLabel={(row) => `„${row.number}“ löschen`}
|
||||
/>
|
||||
|
||||
@@ -24,6 +24,9 @@ import { primaryPrice, priceBrutto } from '../../utils/articleDetail';
|
||||
import artikelAutomat, { createArticleSelector, getArticle } from '../../automats/artikelAutomat';
|
||||
import { artikelDetailPath } from '../article/articleRoute';
|
||||
import { BrandLabel } from '../brand';
|
||||
import {
|
||||
BORDER,
|
||||
} from '../../colors';
|
||||
|
||||
class ManifestFormLine extends PureComponent {
|
||||
constructor(props) {
|
||||
@@ -219,7 +222,6 @@ class ManifestFormLine extends PureComponent {
|
||||
htmlInput: {
|
||||
inputMode: type === 'number' ? 'decimal' : 'text',
|
||||
style: { textAlign: align },
|
||||
spellCheck: false,
|
||||
autoComplete: 'off',
|
||||
step: type === 'number' ? 'any' : undefined,
|
||||
},
|
||||
@@ -323,7 +325,7 @@ class ManifestFormLine extends PureComponent {
|
||||
minWidth: 28,
|
||||
borderRadius: 0.75,
|
||||
overflow: 'hidden',
|
||||
bgcolor: '#e2e8f0',
|
||||
bgcolor: BORDER,
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
justifyContent: 'center',
|
||||
|
||||
@@ -15,6 +15,9 @@ import FileDropzone from '../FileDropzone';
|
||||
import AttachmentList from './AttachmentList';
|
||||
import { hasArticleLines, manifestConfig, partyDisplayName } from './manifestConfig';
|
||||
import { notifyErrorMessage } from '../../utils/notify';
|
||||
import {
|
||||
SURFACE_RAISED,
|
||||
} from '../../colors';
|
||||
|
||||
class ManifestHeader extends PureComponent {
|
||||
constructor(props) {
|
||||
@@ -136,7 +139,6 @@ class ManifestHeader extends PureComponent {
|
||||
autoComplete="off"
|
||||
slotProps={{
|
||||
inputLabel: { shrink: true },
|
||||
htmlInput: { spellCheck: false },
|
||||
}}
|
||||
sx={{
|
||||
flex: { xs: '1 1 100%', sm: '1 1 150px' },
|
||||
@@ -165,7 +167,6 @@ class ManifestHeader extends PureComponent {
|
||||
...params.slotProps,
|
||||
htmlInput: {
|
||||
...params.slotProps?.htmlInput,
|
||||
spellCheck: false,
|
||||
autoComplete: 'off',
|
||||
},
|
||||
}}
|
||||
@@ -247,7 +248,7 @@ class ManifestHeader extends PureComponent {
|
||||
<PanelTile
|
||||
wide={1}
|
||||
minHeight={160}
|
||||
color={actionsColor ?? '#f1f5f9'}
|
||||
color={actionsColor ?? SURFACE_RAISED}
|
||||
sx={{
|
||||
gridColumn: { xs: '1 / -1', sm: '1 / -1', md: 'span 1' },
|
||||
}}
|
||||
|
||||
@@ -15,6 +15,11 @@ import ViewSidebarIcon from '@mui/icons-material/ViewSidebar';
|
||||
import { enqueueSnackbar } from 'notistack';
|
||||
import { PanelTile } from 'dashgui/panels';
|
||||
import { delivery as deliveryApi } from '../../api';
|
||||
import {
|
||||
SURFACE_BASE,
|
||||
SURFACE_WASH,
|
||||
WHITE,
|
||||
} from '../../colors';
|
||||
|
||||
/**
|
||||
* Inline PDF viewer rendered as a panel tile, mirroring TextEditorPanel's
|
||||
@@ -125,7 +130,7 @@ export class PdfViewerPanel extends PureComponent {
|
||||
<PanelTile
|
||||
wide={wide}
|
||||
minHeight={320}
|
||||
color="#f8fafc"
|
||||
color={SURFACE_BASE}
|
||||
sx={{ height, minHeight: 320 }}
|
||||
>
|
||||
<Box
|
||||
@@ -205,7 +210,7 @@ export class PdfViewerPanel extends PureComponent {
|
||||
border: '1px solid',
|
||||
borderColor: 'divider',
|
||||
borderRadius: 1,
|
||||
bgcolor: '#ffffff',
|
||||
bgcolor: WHITE,
|
||||
overflow: 'hidden',
|
||||
}}
|
||||
>
|
||||
@@ -217,7 +222,7 @@ export class PdfViewerPanel extends PureComponent {
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
justifyContent: 'center',
|
||||
bgcolor: 'rgba(248, 250, 252, 0.7)',
|
||||
bgcolor: SURFACE_WASH,
|
||||
zIndex: 2,
|
||||
}}
|
||||
>
|
||||
|
||||
@@ -18,6 +18,11 @@ import ViewSidebarIcon from '@mui/icons-material/ViewSidebar';
|
||||
import { enqueueSnackbar } from 'notistack';
|
||||
import { PanelTile } from 'dashgui/panels';
|
||||
import { delivery as deliveryApi } from '../../api';
|
||||
import {
|
||||
SURFACE_BASE,
|
||||
SURFACE_WASH,
|
||||
WHITE,
|
||||
} from '../../colors';
|
||||
|
||||
/**
|
||||
* Inline text-file viewer/editor rendered as a full-width panel tile below the
|
||||
@@ -169,7 +174,7 @@ export class TextEditorPanel extends PureComponent {
|
||||
<PanelTile
|
||||
wide={wide}
|
||||
minHeight={320}
|
||||
color="#f8fafc"
|
||||
color={SURFACE_BASE}
|
||||
sx={{ height, minHeight: 320 }}
|
||||
>
|
||||
<Box
|
||||
@@ -264,7 +269,7 @@ export class TextEditorPanel extends PureComponent {
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
justifyContent: 'center',
|
||||
bgcolor: 'rgba(248, 250, 252, 0.7)',
|
||||
bgcolor: SURFACE_WASH,
|
||||
zIndex: 2,
|
||||
}}
|
||||
>
|
||||
@@ -292,7 +297,7 @@ export class TextEditorPanel extends PureComponent {
|
||||
fontSize: '0.72rem',
|
||||
lineHeight: 1.45,
|
||||
tabSize: 4,
|
||||
bgcolor: '#ffffff',
|
||||
bgcolor: WHITE,
|
||||
p: 1.5,
|
||||
'& textarea': {
|
||||
height: '100% !important',
|
||||
|
||||
@@ -9,6 +9,12 @@ import customergroupAutomat from '../../automats/customergroupAutomat';
|
||||
import deliveryAutomat from '../../automats/deliveryAutomat';
|
||||
import quoteAutomat from '../../automats/quoteAutomat';
|
||||
import orderAutomat from '../../automats/orderAutomat';
|
||||
import {
|
||||
GREEN_PANEL,
|
||||
ORANGE_PANEL,
|
||||
PURPLE_PANEL,
|
||||
YELLOW_PANEL,
|
||||
} from '../../colors';
|
||||
|
||||
/** Groups that are not sales customers (filter client-side). */
|
||||
export const EXCLUDED_CUSTOMER_GROUP_NAMES = new Set(['Lieferanten', 'POS']);
|
||||
@@ -559,9 +565,9 @@ export function manifestConfig(kind) {
|
||||
getPathLabel: 'GET /api/delivery/:id',
|
||||
postPathLabel: 'POST /api/delivery',
|
||||
patchPathLabel: 'PATCH /api/delivery/:id',
|
||||
listColor: '#fed7aa',
|
||||
headerColor: '#fed7aa',
|
||||
linesColor: '#bbf7d0',
|
||||
listColor: ORANGE_PANEL,
|
||||
headerColor: ORANGE_PANEL,
|
||||
linesColor: GREEN_PANEL,
|
||||
requirePartyBeforeLines: false,
|
||||
listAutomat: deliveryAutomat,
|
||||
partiesByIdFromList: (parties) =>
|
||||
@@ -595,9 +601,9 @@ export function manifestConfig(kind) {
|
||||
getPathLabel: 'GET /api/quote/:id',
|
||||
postPathLabel: 'POST /api/quote',
|
||||
patchPathLabel: 'PATCH /api/quote/:id',
|
||||
listColor: '#fde68a',
|
||||
headerColor: '#fde68a',
|
||||
linesColor: '#bbf7d0',
|
||||
listColor: YELLOW_PANEL,
|
||||
headerColor: YELLOW_PANEL,
|
||||
linesColor: GREEN_PANEL,
|
||||
requirePartyBeforeLines: false,
|
||||
listAutomat: quoteAutomat,
|
||||
partiesByIdFromList: (parties) =>
|
||||
@@ -628,9 +634,9 @@ export function manifestConfig(kind) {
|
||||
getPathLabel: 'GET /api/order/:id',
|
||||
postPathLabel: 'POST /api/order',
|
||||
patchPathLabel: 'PATCH /api/order/:id',
|
||||
listColor: '#c4b5fd',
|
||||
headerColor: '#c4b5fd',
|
||||
linesColor: '#bbf7d0',
|
||||
listColor: PURPLE_PANEL,
|
||||
headerColor: PURPLE_PANEL,
|
||||
linesColor: GREEN_PANEL,
|
||||
requirePartyBeforeLines: false,
|
||||
listAutomat: orderAutomat,
|
||||
partiesByIdFromList: (parties) =>
|
||||
|
||||
@@ -14,6 +14,16 @@ import {
|
||||
} from '../article/ThumbHashImage';
|
||||
import { BrandLabel } from '../brand';
|
||||
import { thumb } from '../../api';
|
||||
import {
|
||||
GREEN,
|
||||
ICE,
|
||||
INK_STRONG,
|
||||
SCRIM_LABEL,
|
||||
SCRIM_TILE,
|
||||
TEXT_GLOW_DARK,
|
||||
TEXT_GLOW_LIGHT,
|
||||
WHITE,
|
||||
} from '../../colors';
|
||||
|
||||
const MIN_EDGE_STRETCH = 10;
|
||||
|
||||
@@ -85,7 +95,7 @@ function drawContainedWithStretchedEdges(
|
||||
const grow = maxBlur * 0.5;
|
||||
lctx.globalCompositeOperation = 'destination-in';
|
||||
lctx.filter = `blur(${maxBlur}px)`;
|
||||
lctx.fillStyle = '#000';
|
||||
lctx.fillStyle = INK_STRONG;
|
||||
lctx.fillRect(
|
||||
offsetX - grow,
|
||||
offsetY - grow,
|
||||
@@ -325,7 +335,7 @@ export class ArticleTile extends Tile {
|
||||
px: 0.75,
|
||||
py: 0.25,
|
||||
borderRadius: 999,
|
||||
bgcolor: 'rgba(0, 0, 0, 0.65)',
|
||||
bgcolor: SCRIM_LABEL,
|
||||
color: 'common.white',
|
||||
fontSize: 11,
|
||||
fontWeight: 600,
|
||||
@@ -389,10 +399,10 @@ export class ArticleTile extends Tile {
|
||||
bottom: 0,
|
||||
px: 1,
|
||||
py: 0.75,
|
||||
background: 'linear-gradient(transparent, rgba(0,0,0,0.65))',
|
||||
background: SCRIM_TILE,
|
||||
textAlign: 'left',
|
||||
color: '#fff',
|
||||
textShadow: '0 1px 3px rgba(0, 0, 0, 0.95), 0 0 2px rgba(0, 0, 0, 0.85)',
|
||||
color: WHITE,
|
||||
textShadow: TEXT_GLOW_DARK,
|
||||
}}
|
||||
>
|
||||
{article?.brand?.name ? (
|
||||
@@ -405,11 +415,11 @@ export class ArticleTile extends Tile {
|
||||
variant: 'caption',
|
||||
noWrap: true,
|
||||
sx: {
|
||||
color: '#b3e5fc',
|
||||
color: ICE,
|
||||
opacity: 1,
|
||||
fontWeight: 500,
|
||||
lineHeight: 1.1,
|
||||
textShadow: '0 1px 3px rgba(0, 0, 0, 0.95), 0 0 2px rgba(0, 0, 0, 0.85)',
|
||||
textShadow: TEXT_GLOW_DARK,
|
||||
},
|
||||
}}
|
||||
sx={{ mb: 0.25 }}
|
||||
@@ -421,9 +431,9 @@ export class ArticleTile extends Tile {
|
||||
fontWeight={600}
|
||||
noWrap
|
||||
sx={{
|
||||
color: '#111',
|
||||
color: INK_STRONG,
|
||||
lineHeight: 1.2,
|
||||
textShadow: '0 0 3px rgba(255, 255, 255, 1), 0 0 6px rgba(255, 255, 255, 0.9), 0 1px 2px rgba(255, 255, 255, 0.9)',
|
||||
textShadow: TEXT_GLOW_LIGHT,
|
||||
}}
|
||||
>
|
||||
{label}
|
||||
@@ -434,8 +444,8 @@ export class ArticleTile extends Tile {
|
||||
variant="caption"
|
||||
noWrap
|
||||
sx={{
|
||||
color: '#ffffff',
|
||||
textShadow: '0 1px 3px rgba(0, 0, 0, 0.95), 0 0 2px rgba(0, 0, 0, 0.85)',
|
||||
color: WHITE,
|
||||
textShadow: TEXT_GLOW_DARK,
|
||||
}}
|
||||
>
|
||||
{sku}
|
||||
@@ -447,8 +457,8 @@ export class ArticleTile extends Tile {
|
||||
fontWeight={600}
|
||||
component="div"
|
||||
sx={{
|
||||
color: '#b9f6ca',
|
||||
textShadow: '0 1px 3px rgba(0, 0, 0, 0.95), 0 0 2px rgba(0, 0, 0, 0.85)',
|
||||
color: GREEN,
|
||||
textShadow: TEXT_GLOW_DARK,
|
||||
}}
|
||||
>
|
||||
{formatPrice(article.net)}
|
||||
|
||||
@@ -12,6 +12,12 @@ import DeleteOutlinedIcon from '@mui/icons-material/DeleteOutlined';
|
||||
import { getThumbHashDataUrl, isImageLoaded, markImageLoaded } from '../article/ThumbHashImage';
|
||||
import { thumb } from '../../api';
|
||||
import { tileBaseSx } from 'dashgui/tiles';
|
||||
import {
|
||||
RED_TINT,
|
||||
SCRIM_BADGE,
|
||||
SCRIM_MEDIUM,
|
||||
VEIL_HOVER,
|
||||
} from '../../colors';
|
||||
|
||||
/**
|
||||
* Top row (1/3): sort handle | settings toggle | move handle.
|
||||
@@ -28,7 +34,7 @@ function TopBand({ onSettingsClick, settingsTab, terminalCount, onPlacementClick
|
||||
top: '50%',
|
||||
transform: 'translateY(-50%)',
|
||||
color: 'common.white',
|
||||
'&:hover': { bgcolor: 'rgba(255, 255, 255, 0.15)' },
|
||||
'&:hover': { bgcolor: VEIL_HOVER },
|
||||
};
|
||||
|
||||
return (
|
||||
@@ -39,7 +45,7 @@ function TopBand({ onSettingsClick, settingsTab, terminalCount, onPlacementClick
|
||||
left: 0,
|
||||
right: 0,
|
||||
height: '33.33%',
|
||||
bgcolor: 'rgba(0, 0, 0, 0.45)',
|
||||
bgcolor: SCRIM_MEDIUM,
|
||||
}}
|
||||
>
|
||||
<IconButton
|
||||
@@ -115,7 +121,7 @@ function MiddleBand({ settingsTab, nameInput, terminals, categoryPosterminalIds,
|
||||
...(isActionTab
|
||||
? {
|
||||
'&:hover': {
|
||||
bgcolor: settingsTab === -3 ? 'rgba(211, 47, 47, 0.12)' : 'grey.400',
|
||||
bgcolor: settingsTab === -3 ? RED_TINT : 'grey.400',
|
||||
},
|
||||
}
|
||||
: {}),
|
||||
@@ -332,7 +338,7 @@ export class CategoryTile extends Component {
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
justifyContent: 'center',
|
||||
bgcolor: 'rgba(0, 0, 0, 0.5)',
|
||||
bgcolor: SCRIM_BADGE,
|
||||
px: 1,
|
||||
}}
|
||||
>
|
||||
|
||||
@@ -1,11 +1,13 @@
|
||||
@import 'ag-grid-community/styles/ag-grid.css';
|
||||
@import 'ag-grid-community/styles/ag-theme-alpine.css';
|
||||
|
||||
/* App background. Provided as a CSS custom property by src/colors.js
|
||||
(installCssVars), with a hard-coded fallback for the very first paint. */
|
||||
html,
|
||||
body,
|
||||
#root {
|
||||
min-height: 100%;
|
||||
background-color: #85b7ad;
|
||||
background-color: var(--df-main-background, #82b8b4);
|
||||
}
|
||||
|
||||
html,
|
||||
@@ -14,20 +16,60 @@ body {
|
||||
touch-action: manipulation;
|
||||
}
|
||||
|
||||
/* ────────────────────────────────────────────────────────────────────────────
|
||||
* SYSTEM-WIDE SPELLCHECK OFF
|
||||
*
|
||||
* dashfront is a German ERP/POS for article numbers, EANs, SKUs and quantities.
|
||||
* None of that vocabulary exists in any dictionary, so a spellchecker only ever
|
||||
* paints red squiggles and slows typing. Every editable surface must therefore
|
||||
* opt out — including inputs the app does not own (ag-grid filter boxes, and
|
||||
* any input a browser or extension injects into the page).
|
||||
*
|
||||
* The `spellcheck` content attribute is the only switch: it is the default for
|
||||
* every <input>/<textarea>, and browsers honour it as a presentational hint
|
||||
* regardless of the element's own attribute.
|
||||
*
|
||||
* `TextInput` (src/components/TextInput.jsx) additionally sets spellCheck=false
|
||||
* per input, which covers fields React re-renders. This rule is the backstop
|
||||
* that also covers DOM we do not create.
|
||||
* ──────────────────────────────────────────────────────────────────────────── */
|
||||
input,
|
||||
textarea,
|
||||
[contenteditable] {
|
||||
spellcheck: false;
|
||||
}
|
||||
|
||||
html {
|
||||
scrollbar-gutter: stable;
|
||||
}
|
||||
|
||||
/* SortableJS: Kategorie-Reorder im Edit-Modus */
|
||||
/* SortableJS: Kategorie-Reorder im Edit-Modus.
|
||||
Colors come from CSS custom properties injected by src/colors.js
|
||||
(installCssVars), so this file stays in sync with the JS token module.
|
||||
Plain CSS cannot import the JS module directly. */
|
||||
.sortable-ghost {
|
||||
outline: 2px dashed #2e7d32;
|
||||
opacity: 0.35 !important;
|
||||
outline: 2px dashed var(--df-sort-ghost, #15803d);
|
||||
outline-offset: 2px;
|
||||
background-color: rgba(46, 125, 50, 0.12) !important;
|
||||
background-color: var(--df-sort-ghost-tint, rgba(21, 128, 61, 0.12)) !important;
|
||||
}
|
||||
|
||||
.sortable-chosen {
|
||||
box-shadow: var(--df-shadow-sortable, 0 4px 16px rgba(15, 23, 42, 0.12));
|
||||
}
|
||||
|
||||
.drag-handle {
|
||||
cursor: grab;
|
||||
touch-action: none;
|
||||
}
|
||||
|
||||
.drag-handle:active {
|
||||
cursor: grabbing;
|
||||
}
|
||||
|
||||
.sortable-drag {
|
||||
opacity: 1 !important;
|
||||
box-shadow: 0 8px 20px rgba(0, 0, 0, 0.28);
|
||||
box-shadow: var(--df-shadow-drag, 0 8px 20px rgba(15, 23, 42, 0.28));
|
||||
cursor: grabbing !important;
|
||||
user-select: none !important;
|
||||
}
|
||||
@@ -7,11 +7,16 @@ import { SnackbarProvider } from 'notistack';
|
||||
import AuthBootstrap from './components/AuthBootstrap';
|
||||
import App from './App';
|
||||
import { getActiveTheme } from './theme';
|
||||
import { installCssVars } from './colors';
|
||||
// Side-effect imports: each module registers its automat and subscribes to
|
||||
// authAutomat, so the stores must be evaluated before the session bootstraps.
|
||||
import './automats';
|
||||
import './index.css';
|
||||
|
||||
// Publish the color tokens as CSS custom properties so index.css (and any
|
||||
// other plain-CSS stylesheet) reads the same palette as the React components.
|
||||
installCssVars();
|
||||
|
||||
// Dev-only Automat inspector. The `import.meta.env.DEV` branch is statically
|
||||
// eliminated in production builds, so this module never ships to users.
|
||||
const AutomatInspector = import.meta.env.DEV
|
||||
|
||||
@@ -22,6 +22,7 @@ import PublicIcon from '@mui/icons-material/Public';
|
||||
import BarChartIcon from '@mui/icons-material/BarChart';
|
||||
import { isKategorienPath } from './components/category/categoryRoute';
|
||||
import { deliveryEditorAutomat } from './automats/deliveryEditorAutomat';
|
||||
import { NAV_ITEM_ACTIVE_ACCENT } from './colors';
|
||||
|
||||
class DeliveryNavIcon extends PureComponent {
|
||||
constructor(props) {
|
||||
@@ -53,7 +54,7 @@ class DeliveryNavIcon extends PureComponent {
|
||||
...rest,
|
||||
sx: [
|
||||
...(Array.isArray(sx) ? sx : sx ? [sx] : []),
|
||||
(theme) => (hasDraft ? { color: theme.nav?.itemActiveAccent || '#2dd4bf' } : null),
|
||||
(theme) => (hasDraft ? { color: theme.nav?.itemActiveAccent || NAV_ITEM_ACTIVE_ACCENT } : null),
|
||||
],
|
||||
});
|
||||
}
|
||||
|
||||
@@ -32,6 +32,9 @@ import { artikelDetailPath } from '../components/article/articleRoute';
|
||||
import withSearchParams from '../components/article/withSearchParams';
|
||||
import { setLastArticleSku } from '../automats';
|
||||
import { BrandLabel } from '../components/brand';
|
||||
import {
|
||||
BLUE_BG,
|
||||
} from '../colors';
|
||||
|
||||
const resultRowSx = {
|
||||
alignItems: 'center',
|
||||
@@ -276,7 +279,6 @@ class ArtikelSearchPage extends Component {
|
||||
onChange={this.handleInputChange}
|
||||
slotProps={{
|
||||
htmlInput: {
|
||||
spellCheck: false,
|
||||
autoCapitalize: 'off',
|
||||
autoCorrect: 'off',
|
||||
},
|
||||
@@ -373,7 +375,7 @@ class ArtikelSearchPage extends Component {
|
||||
measureElement={
|
||||
<Box
|
||||
sx={{
|
||||
bgcolor: '#dbeafe',
|
||||
bgcolor: BLUE_BG,
|
||||
p: 2,
|
||||
borderRadius: 1,
|
||||
textAlign: 'center',
|
||||
|
||||
@@ -498,7 +498,10 @@ class ArtikelKategorienPage extends PureComponent {
|
||||
}}
|
||||
variant="standard"
|
||||
size="small"
|
||||
slotProps={{ inputLabel: { shrink: true }, input: { spellCheck: false } }}
|
||||
slotProps={{
|
||||
inputLabel: { shrink: true },
|
||||
htmlInput: { spellCheck: false },
|
||||
}}
|
||||
sx={{
|
||||
width: '100%',
|
||||
'& .MuiInput-underline:before, & .MuiInput-underline:after': {
|
||||
|
||||
@@ -21,6 +21,9 @@ import { backendErrorFrom } from '../../utils/backendError';
|
||||
import { notifyError } from '../../utils/notify';
|
||||
import { setLastArticleSku } from '../../automats';
|
||||
import warehouseAutomat from '../../automats/warehouseAutomat';
|
||||
import {
|
||||
GREEN_PANEL,
|
||||
} from '../../colors';
|
||||
|
||||
function draftsFrom(warehouses) {
|
||||
return Object.fromEntries(warehouses.map((row) => [row.id, row.name]));
|
||||
@@ -285,15 +288,9 @@ class ArtikelLagerPage extends PureComponent {
|
||||
return (
|
||||
<>
|
||||
{/* SortableJS ghost / chosen styling */}
|
||||
<style>{`
|
||||
.sortable-ghost { opacity: 0.35 !important; }
|
||||
.sortable-chosen { box-shadow: 0 4px 16px rgba(0,0,0,0.12); }
|
||||
.drag-handle { cursor: grab; touch-action: none; }
|
||||
.drag-handle:active { cursor: grabbing; }
|
||||
`}</style>
|
||||
|
||||
<PanelTileGrid>
|
||||
<PanelTile wide={2} minHeight={280} color="#bbf7d0">
|
||||
<PanelTile wide={2} minHeight={280} color={GREEN_PANEL}>
|
||||
<Stack spacing={2}>
|
||||
{returnTo ? (
|
||||
<Alert
|
||||
|
||||
@@ -19,6 +19,9 @@ import { setLastArticleSku } from '../../automats';
|
||||
import brandAutomat from '../../automats/brandAutomat';
|
||||
import { enqueueSnackbar } from 'notistack';
|
||||
import { BrandThumb, BrandImageSearchDialog } from '../../components/brand';
|
||||
import {
|
||||
PURPLE_PANEL,
|
||||
} from '../../colors';
|
||||
|
||||
function draftsFrom(brands) {
|
||||
return Object.fromEntries(brands.map((b) => [b.id, b.name]));
|
||||
@@ -180,7 +183,7 @@ class ArtikelMarkenPage extends PureComponent {
|
||||
|
||||
return (
|
||||
<PanelTileGrid>
|
||||
<PanelTile wide={2} minHeight={280} color="#ddd6fe">
|
||||
<PanelTile wide={2} minHeight={280} color={PURPLE_PANEL}>
|
||||
<List dense disablePadding>
|
||||
{brands.length === 0 ? (
|
||||
<Typography variant="body2" color="text.secondary" sx={{ py: 1 }}>
|
||||
|
||||
@@ -24,6 +24,15 @@ import { notifyError } from '../../utils/notify';
|
||||
import { setLastArticleSku } from '../../automats';
|
||||
import attributeAutomat, { selectAttributes } from '../../automats/attributeAutomat';
|
||||
import propertyAutomat, { selectProperties } from '../../automats/propertyAutomat';
|
||||
import {
|
||||
BLUE_PANEL,
|
||||
ORANGE_PANEL,
|
||||
PURPLE_PANEL,
|
||||
SELECT_BG,
|
||||
SELECT_BG_HOVER,
|
||||
SELECT_BORDER,
|
||||
TINT_SUBTLE,
|
||||
} from '../../colors';
|
||||
|
||||
function draftsFrom(items, key = 'name') {
|
||||
return Object.fromEntries((items || []).map((row) => [row.id, row[key]]));
|
||||
@@ -609,16 +618,10 @@ class ArtikelMerkmalePage extends PureComponent {
|
||||
return (
|
||||
<>
|
||||
{/* SortableJS ghost / chosen styling from /artikel/lager */}
|
||||
<style>{`
|
||||
.sortable-ghost { opacity: 0.35 !important; }
|
||||
.sortable-chosen { box-shadow: 0 4px 16px rgba(0,0,0,0.12); }
|
||||
.drag-handle { cursor: grab; touch-action: none; }
|
||||
.drag-handle:active { cursor: grabbing; }
|
||||
`}</style>
|
||||
|
||||
<PanelTileGrid>
|
||||
{/* Tile 1: Attribute (Auswahllisten) */}
|
||||
<PanelTile wide={2} minHeight={280} color="#bfdbfe">
|
||||
<PanelTile wide={2} minHeight={280} color={BLUE_PANEL}>
|
||||
<Stack spacing={2}>
|
||||
<Box sx={{ display: 'flex', alignItems: 'center', gap: 1 }}>
|
||||
<StyleIcon sx={{ color: 'text.secondary', fontSize: 20 }} />
|
||||
@@ -642,11 +645,11 @@ class ArtikelMerkmalePage extends PureComponent {
|
||||
borderRadius: 1.5,
|
||||
alignItems: 'center',
|
||||
cursor: 'pointer',
|
||||
bgcolor: isSelected ? 'rgba(59, 130, 246, 0.12)' : 'transparent',
|
||||
border: isSelected ? '1px solid rgba(59, 130, 246, 0.3)' : '1px solid transparent',
|
||||
bgcolor: isSelected ? SELECT_BG : 'transparent',
|
||||
border: isSelected ? `1px solid ${SELECT_BORDER}` : '1px solid transparent',
|
||||
transition: 'all 0.15s ease',
|
||||
'&:hover': {
|
||||
bgcolor: isSelected ? 'rgba(59, 130, 246, 0.16)' : 'rgba(0, 0, 0, 0.04)',
|
||||
bgcolor: isSelected ? SELECT_BG_HOVER : TINT_SUBTLE,
|
||||
},
|
||||
}}
|
||||
data-id={a.id}
|
||||
@@ -764,7 +767,7 @@ class ArtikelMerkmalePage extends PureComponent {
|
||||
</PanelTile>
|
||||
|
||||
{/* Tile 2: Werte für das gewählte Attribut */}
|
||||
<PanelTile wide={2} minHeight={280} color="#ddd6fe">
|
||||
<PanelTile wide={2} minHeight={280} color={PURPLE_PANEL}>
|
||||
<Stack spacing={2}>
|
||||
<Box sx={{ display: 'flex', alignItems: 'center', gap: 1 }}>
|
||||
<LabelImportantIcon sx={{ color: 'text.secondary', fontSize: 20 }} />
|
||||
@@ -867,7 +870,7 @@ class ArtikelMerkmalePage extends PureComponent {
|
||||
</PanelTile>
|
||||
|
||||
{/* Tile 3: Merkmale (Freitext) */}
|
||||
<PanelTile wide={2} minHeight={280} color="#fed7aa">
|
||||
<PanelTile wide={2} minHeight={280} color={ORANGE_PANEL}>
|
||||
<Stack spacing={2}>
|
||||
<Box sx={{ display: 'flex', alignItems: 'center', gap: 1 }}>
|
||||
<TuneIcon sx={{ color: 'text.secondary', fontSize: 20 }} />
|
||||
|
||||
@@ -25,6 +25,10 @@ import { notifyError } from '../../utils/notify';
|
||||
import { setLastArticleSku } from '../../automats';
|
||||
import { labeltemplate } from '../../api';
|
||||
import { enqueueSnackbar } from 'notistack';
|
||||
import {
|
||||
BLUE_BG,
|
||||
SURFACE_BASE,
|
||||
} from '../../colors';
|
||||
|
||||
const CanvasEditor = lazy(async () => {
|
||||
await import('react-canvas-label-editor/style.css');
|
||||
@@ -465,7 +469,7 @@ class ArtikelVorlagenPage extends PureComponent {
|
||||
return (
|
||||
<>
|
||||
<PanelTileGrid>
|
||||
<PanelTile wide={3} color="#dbeafe">
|
||||
<PanelTile wide={3} color={BLUE_BG}>
|
||||
<Box sx={{ display: 'flex', alignItems: 'center', justifyContent: 'flex-end', mb: 2 }}>
|
||||
<Button
|
||||
variant="contained"
|
||||
@@ -517,7 +521,7 @@ class ArtikelVorlagenPage extends PureComponent {
|
||||
onClick={() => this.handleOpenEdit(tmpl)}
|
||||
sx={{
|
||||
height: 100,
|
||||
bgcolor: '#f8f8f8',
|
||||
bgcolor: SURFACE_BASE,
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
justifyContent: 'center',
|
||||
|
||||
@@ -8,6 +8,9 @@ import { PanelTile } from 'dashgui/panels';
|
||||
import { AutomatGrid } from 'react-virtual-aggridautomat';
|
||||
import { articleHistoryGridAutomat } from '../../automats';
|
||||
import { router } from '../../appRouter';
|
||||
import {
|
||||
LINK,
|
||||
} from '../../colors';
|
||||
|
||||
const COLUMN_DEFS = [
|
||||
{
|
||||
@@ -43,7 +46,7 @@ const COLUMN_DEFS = [
|
||||
e.preventDefault();
|
||||
void router.navigate(`/artikel/${encodeURIComponent(p.value)}`);
|
||||
}}
|
||||
style={{ color: '#2563eb', fontWeight: 600, textDecoration: 'none' }}
|
||||
style={{ color: LINK, fontWeight: 600, textDecoration: 'none' }}
|
||||
>
|
||||
{p.value}
|
||||
</a>
|
||||
|
||||
@@ -8,6 +8,9 @@ import customergroupAutomat from '../automats/customergroupAutomat';
|
||||
import { router } from '../appRouter';
|
||||
import { backendErrorFrom } from '../utils/backendError';
|
||||
import { notifyError } from '../utils/notify';
|
||||
import {
|
||||
BLUE_PANEL,
|
||||
} from '../colors';
|
||||
|
||||
function sortEntities(entities) {
|
||||
return [...entities].sort((a, b) => {
|
||||
@@ -142,7 +145,7 @@ class KundeListePage extends PureComponent {
|
||||
onDelete={this.handleDelete}
|
||||
deleteDisabled={busyId != null}
|
||||
emptyMessage="Noch keine Kunden."
|
||||
color="#bfdbfe"
|
||||
color={BLUE_PANEL}
|
||||
getEditLabel={(row) => `„${label(row)}“ bearbeiten`}
|
||||
getDeleteLabel={(row) => `„${label(row)}“ löschen`}
|
||||
/>
|
||||
|
||||
@@ -18,6 +18,11 @@ import { requireArray, requireDefined, requireObject } from '../../utils/apiShap
|
||||
import { backendErrorFrom } from '../../utils/backendError';
|
||||
import { notifyError } from '../../utils/notify';
|
||||
import withEntityRoute from '../../components/kunde/withEntityRoute';
|
||||
import {
|
||||
BLUE_PANEL,
|
||||
GREEN_PANEL,
|
||||
YELLOW_PANEL,
|
||||
} from '../../colors';
|
||||
|
||||
const ADDRESS_FIELDS = [
|
||||
'addressType',
|
||||
@@ -551,7 +556,7 @@ class KundeEditPage extends PureComponent {
|
||||
|
||||
return (
|
||||
<PanelTileGrid>
|
||||
<PanelTile wide={2} minHeight={200} color="#bfdbfe" title="Stammdaten">
|
||||
<PanelTile wide={2} minHeight={200} color={BLUE_PANEL} title="Stammdaten">
|
||||
<Stack spacing={2}>
|
||||
{groupName != null ? (
|
||||
<Typography variant="body2" color="text.secondary">
|
||||
@@ -586,7 +591,7 @@ class KundeEditPage extends PureComponent {
|
||||
</Stack>
|
||||
</PanelTile>
|
||||
|
||||
<PanelTile wide={3} minHeight={240} color="#bbf7d0" title="Adressen">
|
||||
<PanelTile wide={3} minHeight={240} color={GREEN_PANEL} title="Adressen">
|
||||
<Stack spacing={2}>
|
||||
{addresses.length === 0 ? (
|
||||
<Typography variant="body2" color="text.secondary">
|
||||
@@ -656,7 +661,7 @@ class KundeEditPage extends PureComponent {
|
||||
</Stack>
|
||||
</PanelTile>
|
||||
|
||||
<PanelTile wide={3} minHeight={200} color="#fde68a" title="Kontakte">
|
||||
<PanelTile wide={3} minHeight={200} color={YELLOW_PANEL} title="Kontakte">
|
||||
<Stack spacing={2}>
|
||||
{contacts.length === 0 ? (
|
||||
<Typography variant="body2" color="text.secondary">
|
||||
|
||||
@@ -20,6 +20,9 @@ import customergroupAutomat from '../../automats/customergroupAutomat';
|
||||
import { requireArray, requireDefined, requireObject } from '../../utils/apiShape';
|
||||
import { backendErrorFrom } from '../../utils/backendError';
|
||||
import { notifyError } from '../../utils/notify';
|
||||
import {
|
||||
PURPLE_BG,
|
||||
} from '../../colors';
|
||||
|
||||
function sortGroups(groups) {
|
||||
return [...groups].sort((a, b) => {
|
||||
@@ -231,7 +234,7 @@ class KundeGruppenPage extends PureComponent {
|
||||
|
||||
return (
|
||||
<PanelTileGrid>
|
||||
<PanelTile wide={2} minHeight={280} color="#e9d5ff">
|
||||
<PanelTile wide={2} minHeight={280} color={PURPLE_BG}>
|
||||
<Stack spacing={2}>
|
||||
<List dense disablePadding>
|
||||
{groups.length === 0 ? (
|
||||
|
||||
@@ -14,6 +14,9 @@ import customergroupAutomat from '../../automats/customergroupAutomat';
|
||||
import { router } from '../../appRouter';
|
||||
import { requireDefined, requireObject } from '../../utils/apiShape';
|
||||
import { notifyError } from '../../utils/notify';
|
||||
import {
|
||||
BLUE_PANEL,
|
||||
} from '../../colors';
|
||||
|
||||
class KundeNeuPage extends PureComponent {
|
||||
constructor(props) {
|
||||
@@ -95,7 +98,7 @@ class KundeNeuPage extends PureComponent {
|
||||
|
||||
return (
|
||||
<PanelTileGrid>
|
||||
<PanelTile wide={2} minHeight={220} color="#bfdbfe" title="Neuer Kunde">
|
||||
<PanelTile wide={2} minHeight={220} color={BLUE_PANEL} title="Neuer Kunde">
|
||||
<Stack spacing={2}>
|
||||
<TextField
|
||||
size="small"
|
||||
@@ -103,7 +106,7 @@ class KundeNeuPage extends PureComponent {
|
||||
value={number}
|
||||
disabled
|
||||
slotProps={{
|
||||
htmlInput: { spellCheck: false, autoComplete: 'off' },
|
||||
htmlInput: { autoComplete: 'off' },
|
||||
}}
|
||||
/>
|
||||
<Select
|
||||
@@ -112,9 +115,8 @@ class KundeNeuPage extends PureComponent {
|
||||
disabled={saving}
|
||||
onChange={this.handleGroupChange}
|
||||
displayEmpty
|
||||
inputProps={{
|
||||
spellCheck: false,
|
||||
autoComplete: 'off',
|
||||
slotProps={{
|
||||
htmlInput: { autoComplete: 'off' },
|
||||
}}
|
||||
>
|
||||
{groups.map((row) => (
|
||||
|
||||
@@ -21,6 +21,9 @@ import { notifyError } from '../../utils/notify';
|
||||
import { setLastDeliveryPath } from '../../automats';
|
||||
import supplierAutomat from '../../automats/supplierAutomat';
|
||||
import { deliveryDraftAutomat } from '../../automats/deliveryEditorAutomat';
|
||||
import {
|
||||
GREEN_PANEL,
|
||||
} from '../../colors';
|
||||
|
||||
|
||||
function sortSuppliers(suppliers) {
|
||||
@@ -224,7 +227,7 @@ class LieferungLieferantenPage extends PureComponent {
|
||||
|
||||
return (
|
||||
<PanelTileGrid>
|
||||
<PanelTile wide={2} minHeight={280} color="#bbf7d0">
|
||||
<PanelTile wide={2} minHeight={280} color={GREEN_PANEL}>
|
||||
<Stack spacing={2}>
|
||||
{returnTo ? (
|
||||
<Alert
|
||||
|
||||
@@ -50,6 +50,11 @@ import { getDeliveryEditorAutomat } from '../../automats/deliveryEditorAutomat';
|
||||
import { setLastDeliveryPath, userSettingsAutomat, setViewerPosition } from '../../automats';
|
||||
|
||||
import { fetchDeliveryLedgerSummary } from '../../utils/deliveryLedger';
|
||||
import {
|
||||
SURFACE_BASE,
|
||||
SURFACE_RAISED,
|
||||
YELLOW_BG,
|
||||
} from '../../colors';
|
||||
|
||||
class LieferungNeuPage extends PureComponent {
|
||||
constructor(props) {
|
||||
@@ -1266,7 +1271,7 @@ class LieferungNeuPage extends PureComponent {
|
||||
/>
|
||||
{this.state.viewerPosition === 'top' && this.renderViewerPanels(4)}
|
||||
{this.state.unmatchedSupplierName && (
|
||||
<PanelTile wide={4} color="#fef3c7" sx={{ gridColumn: { xs: '1 / -1', sm: '1 / -1', md: 'span 4' } }}>
|
||||
<PanelTile wide={4} color={YELLOW_BG} sx={{ gridColumn: { xs: '1 / -1', sm: '1 / -1', md: 'span 4' } }}>
|
||||
<Alert
|
||||
severity="warning"
|
||||
action={
|
||||
@@ -1316,7 +1321,7 @@ class LieferungNeuPage extends PureComponent {
|
||||
{this.state.viewerPosition === 'below' && this.renderViewerPanels(4)}
|
||||
{this.props.docId != null && this.renderLedgerPanel()}
|
||||
{notes && notes.length > 0 && (
|
||||
<PanelTile wide={4} color="#fffbeb" sx={{ gridColumn: { xs: '1 / -1', sm: '1 / -1', md: 'span 4' } }}>
|
||||
<PanelTile wide={4} color={YELLOW_BG} sx={{ gridColumn: { xs: '1 / -1', sm: '1 / -1', md: 'span 4' } }}>
|
||||
<Typography variant="subtitle2" fontWeight={700} color="warning.dark" sx={{ mb: 1.5 }}>
|
||||
Hinweise zur PDF-Analyse ({notes.length})
|
||||
</Typography>
|
||||
@@ -1401,7 +1406,7 @@ class LieferungNeuPage extends PureComponent {
|
||||
<PanelTile
|
||||
wide={4}
|
||||
title="Lagerstatus (Ledger)"
|
||||
color="#f8fafc"
|
||||
color={SURFACE_BASE}
|
||||
sx={{
|
||||
gridColumn: { xs: '1 / -1', sm: '1 / -1', md: 'span 4' },
|
||||
border: '1px solid',
|
||||
@@ -1519,7 +1524,7 @@ class LieferungNeuPage extends PureComponent {
|
||||
</Typography>
|
||||
<TableContainer component={Paper} variant="outlined">
|
||||
<Table size="small">
|
||||
<TableHead sx={{ bgcolor: '#f1f5f9' }}>
|
||||
<TableHead sx={{ bgcolor: SURFACE_RAISED }}>
|
||||
<TableRow>
|
||||
<TableCell sx={{ fontWeight: 600, py: 0.5 }}>Zeitpunkt</TableCell>
|
||||
<TableCell sx={{ fontWeight: 600, py: 0.5 }}>Typ</TableCell>
|
||||
|
||||
@@ -1,17 +1,28 @@
|
||||
import { createTheme } from '@mui/material/styles';
|
||||
import { MAIN_BACKGROUND } from '../../colors';
|
||||
import {
|
||||
MAIN_BACKGROUND,
|
||||
NAV_BACKGROUND,
|
||||
NAV_ITEM,
|
||||
NAV_ITEM_ACTIVE,
|
||||
NAV_ITEM_ACTIVE_ACCENT,
|
||||
NAV_ITEM_ACTIVE_TEXT,
|
||||
NAV_ITEM_HOVER,
|
||||
NAV_ITEM_HOVER_TEXT,
|
||||
NAV_SURFACE,
|
||||
PRIMARY,
|
||||
WHITE,
|
||||
} from '../../colors';
|
||||
|
||||
const nav = {
|
||||
background:
|
||||
'linear-gradient(90deg, #183331 0%, #1d3a38 25%, #1a3533 60%, #17302e 100%)',
|
||||
backgroundColor: '#1a3533',
|
||||
background: NAV_BACKGROUND,
|
||||
backgroundColor: NAV_SURFACE,
|
||||
mainBackground: MAIN_BACKGROUND,
|
||||
item: '#9cb8b4',
|
||||
itemHover: '#243f3d',
|
||||
itemHoverText: '#d4ebe8',
|
||||
itemActive: '#2a5a56',
|
||||
itemActiveText: '#6ee7d8',
|
||||
itemActiveAccent: '#2dd4bf',
|
||||
item: NAV_ITEM,
|
||||
itemHover: NAV_ITEM_HOVER,
|
||||
itemHoverText: NAV_ITEM_HOVER_TEXT,
|
||||
itemActive: NAV_ITEM_ACTIVE,
|
||||
itemActiveText: NAV_ITEM_ACTIVE_TEXT,
|
||||
itemActiveAccent: NAV_ITEM_ACTIVE_ACCENT,
|
||||
};
|
||||
|
||||
export const defaultThemeId = 'default';
|
||||
@@ -20,10 +31,10 @@ export function createDefaultTheme() {
|
||||
return createTheme({
|
||||
palette: {
|
||||
mode: 'light',
|
||||
primary: { main: '#0d9488' },
|
||||
primary: { main: PRIMARY },
|
||||
background: {
|
||||
default: nav.mainBackground,
|
||||
paper: '#ffffff',
|
||||
paper: WHITE,
|
||||
},
|
||||
},
|
||||
nav,
|
||||
|
||||
@@ -1,3 +1,9 @@
|
||||
import {
|
||||
BORDER,
|
||||
BORDER_STRONG,
|
||||
TEXT_BODY,
|
||||
WHITE,
|
||||
} from '../colors';
|
||||
let encodeAvif = null;
|
||||
|
||||
async function getAvifEncoder() {
|
||||
@@ -42,20 +48,20 @@ export function renderTextToCanvas(text = '', width = 200, height = 280) {
|
||||
if (!ctx) return canvas;
|
||||
|
||||
// Background
|
||||
ctx.fillStyle = '#ffffff';
|
||||
ctx.fillStyle = WHITE;
|
||||
ctx.fillRect(0, 0, width, height);
|
||||
|
||||
// Subtle border
|
||||
ctx.strokeStyle = '#e2e8f0';
|
||||
ctx.strokeStyle = BORDER;
|
||||
ctx.lineWidth = 1;
|
||||
ctx.strokeRect(0.5, 0.5, width - 1, height - 1);
|
||||
|
||||
// Top header accent line
|
||||
ctx.fillStyle = '#cbd5e1';
|
||||
ctx.fillStyle = BORDER_STRONG;
|
||||
ctx.fillRect(10, 8, 30, 2);
|
||||
|
||||
// Render text lines
|
||||
ctx.fillStyle = '#334155';
|
||||
ctx.fillStyle = TEXT_BODY;
|
||||
ctx.font = '8px ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, "Liberation Mono", "Courier New", monospace';
|
||||
|
||||
const paddingX = 10;
|
||||
|
||||
Reference in New Issue
Block a user