From 822387e4f7c1b524ffe43b6523d5271fd8a2747e Mon Sep 17 00:00:00 2001 From: Claude Date: Mon, 5 Oct 2026 09:37:59 +0000 Subject: [PATCH] Design: UI-Bausteine rendern die neuen Klassen (Button, Badge, Card, Kopf, Reiter, Stat) Co-Authored-By: Claude Sonnet 5.5 --- src/components/ui/AutocompleteInput.jsx | 2 +- src/components/ui/Badge.jsx | 13 +++++----- src/components/ui/Button.jsx | 11 +++------ src/components/ui/Card.jsx | 26 +++++++++---------- src/components/ui/DateTimeInput.jsx | 4 +-- src/components/ui/DurationSelect.jsx | 2 +- src/components/ui/EmptyState.jsx | 8 +++--- src/components/ui/Field.jsx | 6 ++--- src/components/ui/Kpi.jsx | 14 ++++------- src/components/ui/PageHeader.jsx | 33 +++++++++++++++++++------ src/components/ui/Stat.jsx | 10 ++++++++ src/components/ui/StatusPill.jsx | 17 +++---------- src/components/ui/Tabs.jsx | 7 ++++-- src/components/ui/index.js | 1 + src/components/ui/tones.js | 28 +++++++++++++++++++++ tests/unit/uiTones.test.mjs | 20 +++++++++++++++ 16 files changed, 130 insertions(+), 72 deletions(-) create mode 100644 src/components/ui/Stat.jsx create mode 100644 src/components/ui/tones.js create mode 100644 tests/unit/uiTones.test.mjs diff --git a/src/components/ui/AutocompleteInput.jsx b/src/components/ui/AutocompleteInput.jsx index 93109df..330c0b5 100644 --- a/src/components/ui/AutocompleteInput.jsx +++ b/src/components/ui/AutocompleteInput.jsx @@ -38,7 +38,7 @@ export default function AutocompleteInput({ value, onChange, getSuggestions, id, - {dot && } - {children} - - ) +import { badgeToneClass } from './tones' + +// "dot" bleibt als Prop erlaubt und wird ignoriert +export default function Badge({ tone = 'muted', dot, className = '', style, children }) { + const cls = className ? `${badgeToneClass(tone)} ${className}` : badgeToneClass(tone) + return {children} } diff --git a/src/components/ui/Button.jsx b/src/components/ui/Button.jsx index 51482cc..e897833 100644 --- a/src/components/ui/Button.jsx +++ b/src/components/ui/Button.jsx @@ -1,10 +1,6 @@ -const VARIANTS = { - default: 'ui-btn', - primary: 'ui-btn ui-btn-primary', - ghost: 'ui-btn ui-btn-ghost', - danger: 'ui-btn ui-btn-danger', -} +import { BUTTON_CLASSES } from './tones' +// "size" bleibt als Prop erlaubt und wird ignoriert; ein Standard-"type" gibt es nicht export default function Button({ variant = 'default', size, @@ -13,7 +9,8 @@ export default function Button({ children, ...props }) { - const cls = `${VARIANTS[variant] || VARIANTS.default}${size === 'sm' ? ' ui-btn-sm' : ''} ${className}` + const base = Object.prototype.hasOwnProperty.call(BUTTON_CLASSES, variant) ? BUTTON_CLASSES[variant] : BUTTON_CLASSES.default + const cls = className ? `${base} ${className}` : base const Comp = as return ( diff --git a/src/components/ui/Card.jsx b/src/components/ui/Card.jsx index 8abfb62..eb4f582 100644 --- a/src/components/ui/Card.jsx +++ b/src/components/ui/Card.jsx @@ -1,18 +1,14 @@ -export default function Card({ title, actions, children, pad = true, className = '', bodyStyle, style }) { - if (!title && !actions) { - return ( -
- {children} -
- ) - } +// "pad", "bodyStyle" und "style" bleiben als Props erlaubt und werden ignoriert +export default function Card({ title, actions, className, children }) { return ( -
-
-

{title}

- {actions &&
{actions}
} -
-
{children}
-
+
+ {(title || actions) && ( +

+ {title} + {actions && {actions}} +

+ )} + {children} +
) } diff --git a/src/components/ui/DateTimeInput.jsx b/src/components/ui/DateTimeInput.jsx index 39c62c7..f8cdcbd 100644 --- a/src/components/ui/DateTimeInput.jsx +++ b/src/components/ui/DateTimeInput.jsx @@ -15,7 +15,7 @@ export default function DateTimeInput({ id, date, time, onDateChange, onTimeChan onChange(Number(e.target.value))} diff --git a/src/components/ui/EmptyState.jsx b/src/components/ui/EmptyState.jsx index bbdc627..d8c0857 100644 --- a/src/components/ui/EmptyState.jsx +++ b/src/components/ui/EmptyState.jsx @@ -1,10 +1,10 @@ +// "icon" bleibt als Prop erlaubt und wird ignoriert export default function EmptyState({ icon, title, hint, action }) { return (
- {icon &&
{icon}
} -
{title}
- {hint &&
{hint}
} - {action &&
{action}
} + {title} + {hint} + {action &&
{action}
}
) } diff --git a/src/components/ui/Field.jsx b/src/components/ui/Field.jsx index 036dc58..65e04b3 100644 --- a/src/components/ui/Field.jsx +++ b/src/components/ui/Field.jsx @@ -1,9 +1,9 @@ -export default function Field({ label, children, hint }) { +export default function Field({ label, htmlFor, children, hint }) { return (
- {label && } + {label && } {children} - {hint &&
{hint}
} + {hint &&
{hint}
}
) } diff --git a/src/components/ui/Kpi.jsx b/src/components/ui/Kpi.jsx index 9391f41..d2b33d9 100644 --- a/src/components/ui/Kpi.jsx +++ b/src/components/ui/Kpi.jsx @@ -1,10 +1,6 @@ -export default function Kpi({ label, value, sub, icon, accent = 'var(--accent)' }) { - return ( -
- -
{icon}{label}
-
{value}
- {sub &&
{sub}
} -
- ) +import Stat from './Stat' + +// Altname fuer Stat (wird in P7 geloescht); "icon" wird ignoriert, die Warnfarbe wird zu 'neg' +export default function Kpi({ label, value, sub, icon, accent }) { + return } diff --git a/src/components/ui/PageHeader.jsx b/src/components/ui/PageHeader.jsx index 50c98c0..43f531f 100644 --- a/src/components/ui/PageHeader.jsx +++ b/src/components/ui/PageHeader.jsx @@ -1,12 +1,29 @@ -export default function PageHeader({ title, subtitle, actions, children }) { +import { Fragment } from 'react' +import { Link } from 'react-router-dom' + +// crumb: [{ label, to? }] steht vor dem Kopf; Eintraege mit "to" sind Links, die anderen Text +export default function PageHeader({ title, subtitle, actions, eyebrow, crumb, children }) { return ( -
-
-

{title}

- {subtitle &&
{subtitle}
} - {children} + <> + {crumb && crumb.length > 0 && ( + + )} +
+
+ {eyebrow &&
{eyebrow}
} +

{title}

+ {subtitle &&
{subtitle}
} + {children} +
+ {actions &&
{actions}
}
- {actions &&
{actions}
} -
+ ) } diff --git a/src/components/ui/Stat.jsx b/src/components/ui/Stat.jsx new file mode 100644 index 0000000..ce57666 --- /dev/null +++ b/src/components/ui/Stat.jsx @@ -0,0 +1,10 @@ +// Kennzahl: Beschriftung, Wert, Zusatzzeile; tone '' | 'warn' | 'neg'. Der Container .stats kommt vom Aufrufer. +export default function Stat({ label, value, sub, tone = '' }) { + return ( +
+
{label}
+
{value}
+ {sub &&
{sub}
} +
+ ) +} diff --git a/src/components/ui/StatusPill.jsx b/src/components/ui/StatusPill.jsx index 27cb9c1..f55a3a6 100644 --- a/src/components/ui/StatusPill.jsx +++ b/src/components/ui/StatusPill.jsx @@ -1,21 +1,12 @@ -import { statusLabel, statusClass, isUrgent } from '../../lib/ticketStatus' - -const pillBase = { - display: 'inline-flex', - alignItems: 'center', - gap: 6, - padding: '3px 10px', - borderRadius: 999, - fontSize: 12, - fontWeight: 700, -} +import { statusLabel, statusTone, isUrgent } from '../../lib/ticketStatus' export function StatusPill({ status }) { - return {statusLabel(status)} + const tone = statusTone(status) + return {statusLabel(status)} } // Nur dringende Tickets bekommen ein Abzeichen export function UrgentPill({ priority }) { if (!isUrgent(priority)) return null - return Dringend + return Dringend } diff --git a/src/components/ui/Tabs.jsx b/src/components/ui/Tabs.jsx index 2fc5793..ead80bf 100644 --- a/src/components/ui/Tabs.jsx +++ b/src/components/ui/Tabs.jsx @@ -1,14 +1,17 @@ export default function Tabs({ tabs, active, onChange }) { return ( -
+
{tabs.map((t) => ( ))}
diff --git a/src/components/ui/index.js b/src/components/ui/index.js index 450ce02..f6da960 100644 --- a/src/components/ui/index.js +++ b/src/components/ui/index.js @@ -6,6 +6,7 @@ export { StatusPill, UrgentPill } from './StatusPill' export { default as Tabs } from './Tabs' export { default as EmptyState } from './EmptyState' export { default as Field } from './Field' +export { default as Stat } from './Stat' export { default as Kpi } from './Kpi' export { default as AutocompleteInput } from './AutocompleteInput' export { default as DateTimeInput } from './DateTimeInput' diff --git a/src/components/ui/tones.js b/src/components/ui/tones.js new file mode 100644 index 0000000..a4e271b --- /dev/null +++ b/src/components/ui/tones.js @@ -0,0 +1,28 @@ +/* Klassen der Bausteine: reine Zuordnung, ohne JSX (Test: tests/unit/uiTones.test.mjs) */ + +// Badge-Ton (alter und neuer Name) auf die Klasse; "muted"/"grey" und alles Unbekannte = neutral +const BADGE_TONES = { + info: 'blue', + blue: 'blue', + ok: 'green', + green: 'green', + warn: 'orange', + orange: 'orange', + danger: 'red', + red: 'red', + violet: 'violet', + plain: 'plain', +} + +export function badgeToneClass(tone) { + const t = Object.prototype.hasOwnProperty.call(BADGE_TONES, tone) ? BADGE_TONES[tone] : '' + return t ? `badge ${t}` : 'badge' +} + +export const BUTTON_CLASSES = { + default: 'btn', + ghost: 'btn', + primary: 'btn btn-primary', + quiet: 'btn btn-quiet', + danger: 'link-danger', +} diff --git a/tests/unit/uiTones.test.mjs b/tests/unit/uiTones.test.mjs new file mode 100644 index 0000000..6a1544a --- /dev/null +++ b/tests/unit/uiTones.test.mjs @@ -0,0 +1,20 @@ +import test from 'node:test' +import assert from 'node:assert/strict' +import { loadModule } from './load.mjs' + +const { badgeToneClass, BUTTON_CLASSES } = await loadModule('src/components/ui/tones.js') + +test('badgeToneClass', () => { + for (const t of ['muted', 'grey', undefined, 'unbekannt']) assert.equal(badgeToneClass(t), 'badge') + assert.equal(badgeToneClass('info'), 'badge blue') + assert.equal(badgeToneClass('blue'), 'badge blue') + assert.equal(badgeToneClass('ok'), 'badge green') + assert.equal(badgeToneClass('warn'), 'badge orange') + assert.equal(badgeToneClass('danger'), 'badge red') + assert.equal(badgeToneClass('violet'), 'badge violet') + assert.equal(badgeToneClass('plain'), 'badge plain') +}) + +test('Button-Varianten', () => { + assert.deepEqual(BUTTON_CLASSES, { default: 'btn', ghost: 'btn', primary: 'btn btn-primary', quiet: 'btn btn-quiet', danger: 'link-danger' }) +})