feat(ui): one CountBadge for all counters; counters on tab corners
MindSift CI & Docker Release / Build, Test in Container & Push (push) Successful in 2m30s

- CountBadge component (accent / muted, inline / corner, 99+, showZero);
  used in the nav bar, task list and list tabs, admin organizations and
  numeric badges of the mobile menu; old badge-count removed
- Counters sit on the top right corner of tabs; "Alle Aufgaben" gets one
  (desktop tab and mobile menu)
- Open/done task counters include tasks in shared lists, matching what
  "Alle Aufgaben" shows (nav counter was too low)
- Theme template derives --accent-dim/--accent-border from the accent
- Docs: ui-patterns counters section

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
sandrodachandClaude Opus 5.5 committed 2026-10-28 22:54:15 +01:00
1 parent a354f3d008
commit a62b204e14
12 files changed
+167 -55

No files matched your search

+34
View File
@@ -0,0 +1,34 @@
import React from 'react';
interface CountBadgeProps {
count: number | undefined | null;
// accent: something waits for you (nav bar); muted: plain amount (tabs, lists)
variant?: 'accent' | 'muted';
// corner: notification badge on the top right of the nearest `relative` parent
position?: 'inline' | 'corner';
showZero?: boolean;
className?: string; // e.g. to fine-tune the corner offset
}
// Full class names: Tailwind drops component classes it cannot find literally in the source
const VARIANT_CLASS = { accent: 'count-badge-accent', muted: 'count-badge-muted' };
// The one counter of the app (Doku/ui-patterns.md); hides itself at 0 unless showZero
export const CountBadge: React.FC<CountBadgeProps> = ({
count,
variant = 'muted',
position = 'inline',
showZero = false,
className = '',
}) => {
if (count === undefined || count === null || (count <= 0 && !showZero)) return null;
const label = count > 99 ? '99+' : String(count);
return (
<span
className={`count-badge ${VARIANT_CLASS[variant]} ${position === 'corner' ? 'count-badge-corner' : ''} ${className}`}
aria-label={String(count)}
>
{label}
</span>
);
};
+4 -1
View File
@@ -2,6 +2,7 @@ import React from 'react';
import { Check, X } from 'lucide-react';
import { useMobileActions } from '../context/MobileActionContext';
import { triggerHaptic } from '../lib/api';
import { CountBadge } from './CountBadge';
interface MobileHeaderMenuProps {
activeTabLabel: string;
@@ -109,7 +110,9 @@ export const MobileHeaderMenu: React.FC<MobileHeaderMenuProps> = ({ activeTabLab
</div>
<div className="flex items-center gap-2 flex-shrink-0">
{item.badge !== undefined && item.badge !== '' && (
{typeof item.badge === 'number' && !isAccent ? (
<CountBadge count={item.badge} showZero variant={isActive ? 'accent' : 'muted'} />
) : item.badge !== undefined && item.badge !== '' && (
<span
className={`px-2 py-0.5 rounded-full text-xs font-mono font-semibold ${
isAccent
@@ -0,0 +1,23 @@
import { describe, it, expect } from 'vitest';
import { render } from '@testing-library/react';
import { CountBadge } from '../CountBadge';
describe('CountBadge', () => {
it('hides itself at zero unless asked', () => {
expect(render(<CountBadge count={0} />).container.firstChild).toBeNull();
expect(render(<CountBadge count={undefined} />).container.firstChild).toBeNull();
expect(render(<CountBadge count={0} showZero />).container.textContent).toBe('0');
});
it('caps large numbers and keeps the real value for screen readers', () => {
const el = render(<CountBadge count={150} />).container.firstChild as HTMLElement;
expect(el.textContent).toBe('99+');
expect(el.getAttribute('aria-label')).toBe('150');
});
it('maps variant and position to the shared classes', () => {
const el = render(<CountBadge count={3} variant="accent" position="corner" />).container.firstChild as HTMLElement;
expect(el.className).toContain('count-badge-accent');
expect(el.className).toContain('count-badge-corner');
});
});