Skip to content

Commit 6be5393

Browse files
Merge pull request #46 from jeffreylauwers/feature/status-badge
feat(StatusBadge): compact label dat een status communiceert met een signaalkleur
2 parents 19f7142 + 415de74 commit 6be5393

11 files changed

Lines changed: 707 additions & 2 deletions

File tree

Lines changed: 81 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,81 @@
1+
/**
2+
* Status Badge Component
3+
* Compact label that communicates status with a signal color and optional icon.
4+
*
5+
* Usage:
6+
* <!-- Neutral (default), without icon -->
7+
* <strong class="dsn-status-badge">Actief</strong>
8+
*
9+
* <!-- Info variant (with icon) -->
10+
* <strong class="dsn-status-badge dsn-status-badge--info">
11+
* <svg class="dsn-icon dsn-icon--sm" aria-hidden="true"><!-- info-circle --></svg>
12+
* Nieuw
13+
* </strong>
14+
*
15+
* <!-- Positive variant (with icon) -->
16+
* <strong class="dsn-status-badge dsn-status-badge--positive">
17+
* <svg class="dsn-icon dsn-icon--sm" aria-hidden="true"><!-- circle-check --></svg>
18+
* Goedgekeurd
19+
* </strong>
20+
*
21+
* <!-- Negative variant (with icon) -->
22+
* <strong class="dsn-status-badge dsn-status-badge--negative">
23+
* <svg class="dsn-icon dsn-icon--sm" aria-hidden="true"><!-- exclamation-circle --></svg>
24+
* Afgewezen
25+
* </strong>
26+
*
27+
* <!-- Warning variant (with icon) -->
28+
* <strong class="dsn-status-badge dsn-status-badge--warning">
29+
* <svg class="dsn-icon dsn-icon--sm" aria-hidden="true"><!-- alert-triangle --></svg>
30+
* Let op
31+
* </strong>
32+
*/
33+
34+
.dsn-status-badge {
35+
display: inline-flex;
36+
align-items: center;
37+
gap: var(--dsn-status-badge-gap);
38+
font-family: inherit;
39+
font-size: var(--dsn-status-badge-font-size);
40+
line-height: var(--dsn-status-badge-line-height);
41+
text-transform: var(--dsn-status-badge-text-transform);
42+
color: var(--dsn-status-badge-neutral-color);
43+
background-color: var(--dsn-status-badge-neutral-background-color);
44+
border-radius: var(--dsn-status-badge-border-radius);
45+
border: var(--dsn-status-badge-border-width) solid
46+
var(--dsn-status-badge-border-color);
47+
padding-block: var(--dsn-status-badge-padding-block);
48+
padding-inline: var(--dsn-status-badge-padding-inline);
49+
}
50+
51+
/* Info variant */
52+
.dsn-status-badge--info {
53+
color: var(--dsn-status-badge-info-color);
54+
background-color: var(--dsn-status-badge-info-background-color);
55+
}
56+
57+
/* Positive variant */
58+
.dsn-status-badge--positive {
59+
color: var(--dsn-status-badge-positive-color);
60+
background-color: var(--dsn-status-badge-positive-background-color);
61+
}
62+
63+
/* Negative variant */
64+
.dsn-status-badge--negative {
65+
color: var(--dsn-status-badge-negative-color);
66+
background-color: var(--dsn-status-badge-negative-background-color);
67+
}
68+
69+
/* Warning variant */
70+
.dsn-status-badge--warning {
71+
color: var(--dsn-status-badge-warning-color);
72+
background-color: var(--dsn-status-badge-warning-background-color);
73+
}
74+
75+
/* Icon sizing — inherits text color via color: inherit */
76+
.dsn-status-badge > .dsn-icon {
77+
flex-shrink: 0;
78+
width: var(--dsn-icon-size-sm);
79+
height: var(--dsn-icon-size-sm);
80+
color: inherit;
81+
}
Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
/**
2+
* StatusBadge Component Styles for React
3+
* Re-exports the base StatusBadge styles from components-html
4+
*/
5+
6+
@import '../../../components-html/src/status-badge/status-badge.css';
Lines changed: 93 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,93 @@
1+
import { describe, it, expect } from 'vitest';
2+
import { render, screen } from '@testing-library/react';
3+
import { StatusBadge } from './StatusBadge';
4+
5+
describe('StatusBadge', () => {
6+
it('renders children', () => {
7+
render(<StatusBadge>Actief</StatusBadge>);
8+
expect(screen.getByText('Actief')).toBeInTheDocument();
9+
});
10+
11+
it('renders as a <strong> element', () => {
12+
render(<StatusBadge>Status</StatusBadge>);
13+
expect(screen.getByText('Status').tagName).toBe('STRONG');
14+
});
15+
16+
it('always has base dsn-status-badge class', () => {
17+
render(<StatusBadge>Status</StatusBadge>);
18+
expect(screen.getByText('Status')).toHaveClass('dsn-status-badge');
19+
});
20+
21+
it('does not add variant modifier class for neutral variant (default)', () => {
22+
render(<StatusBadge>Status</StatusBadge>);
23+
const el = screen.getByText('Status');
24+
expect(el).not.toHaveClass('dsn-status-badge--neutral');
25+
});
26+
27+
it.each(['positive', 'negative', 'warning', 'info'] as const)(
28+
'applies variant modifier class for %s variant',
29+
(variant) => {
30+
render(<StatusBadge variant={variant}>Status</StatusBadge>);
31+
expect(screen.getByText('Status')).toHaveClass(
32+
`dsn-status-badge--${variant}`
33+
);
34+
}
35+
);
36+
37+
it('does not apply any modifier class for explicit neutral variant', () => {
38+
render(<StatusBadge variant="neutral">Status</StatusBadge>);
39+
const el = screen.getByText('Status');
40+
expect(el).not.toHaveClass('dsn-status-badge--neutral');
41+
});
42+
43+
it('applies custom className', () => {
44+
render(<StatusBadge className="custom">Status</StatusBadge>);
45+
const el = screen.getByText('Status');
46+
expect(el).toHaveClass('dsn-status-badge');
47+
expect(el).toHaveClass('custom');
48+
});
49+
50+
it('renders iconStart before children', () => {
51+
render(
52+
<StatusBadge iconStart={<svg data-testid="icon" />}>Status</StatusBadge>
53+
);
54+
expect(screen.getByTestId('icon')).toBeInTheDocument();
55+
expect(screen.getByText('Status')).toBeInTheDocument();
56+
});
57+
58+
it('renders without iconStart when not provided', () => {
59+
render(<StatusBadge>Status</StatusBadge>);
60+
expect(screen.queryByRole('img')).not.toBeInTheDocument();
61+
});
62+
63+
it('forwards ref', () => {
64+
const ref = { current: null as HTMLElement | null };
65+
render(<StatusBadge ref={ref}>Status</StatusBadge>);
66+
expect(ref.current).toBeInstanceOf(HTMLElement);
67+
});
68+
69+
it('spreads additional HTML attributes', () => {
70+
render(
71+
<StatusBadge id="status-1" data-testid="badge">
72+
Status
73+
</StatusBadge>
74+
);
75+
const el = screen.getByTestId('badge');
76+
expect(el).toHaveAttribute('id', 'status-1');
77+
});
78+
79+
it('renders status label content', () => {
80+
render(<StatusBadge>In behandeling</StatusBadge>);
81+
expect(screen.getByText('In behandeling')).toBeInTheDocument();
82+
});
83+
84+
it('renders approval status content', () => {
85+
render(<StatusBadge variant="positive">Goedgekeurd</StatusBadge>);
86+
expect(screen.getByText('Goedgekeurd')).toBeInTheDocument();
87+
});
88+
89+
it('renders rejection status content', () => {
90+
render(<StatusBadge variant="negative">Afgewezen</StatusBadge>);
91+
expect(screen.getByText('Afgewezen')).toBeInTheDocument();
92+
});
93+
});
Lines changed: 77 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,77 @@
1+
import React from 'react';
2+
import { classNames } from '@dsn/core';
3+
import './StatusBadge.css';
4+
5+
export type StatusBadgeVariant =
6+
| 'neutral'
7+
| 'positive'
8+
| 'negative'
9+
| 'warning'
10+
| 'info';
11+
12+
export interface StatusBadgeProps extends React.HTMLAttributes<HTMLElement> {
13+
/**
14+
* Semantische variant die de signaalkleur bepaalt
15+
* @default 'neutral'
16+
*/
17+
variant?: StatusBadgeVariant;
18+
19+
/**
20+
* Optioneel decoratief icoon vóór de badge-tekst (altijd aria-hidden)
21+
*/
22+
iconStart?: React.ReactNode;
23+
24+
/**
25+
* Badge-tekst (verplicht)
26+
*/
27+
children: React.ReactNode;
28+
}
29+
30+
/**
31+
* StatusBadge component
32+
* Compact label dat een status communiceert met een signaalkleur en optioneel icoon.
33+
*
34+
* @example
35+
* ```tsx
36+
* // Neutral (default), zonder icoon
37+
* <StatusBadge>Actief</StatusBadge>
38+
*
39+
* // Positive met icoon
40+
* <StatusBadge variant="positive" iconStart={<Icon name="circle-check" size="sm" aria-hidden />}>
41+
* Goedgekeurd
42+
* </StatusBadge>
43+
*
44+
* // Negative met icoon
45+
* <StatusBadge variant="negative" iconStart={<Icon name="exclamation-circle" size="sm" aria-hidden />}>
46+
* Afgewezen
47+
* </StatusBadge>
48+
*
49+
* // Info met icoon
50+
* <StatusBadge variant="info" iconStart={<Icon name="info-circle" size="sm" aria-hidden />}>
51+
* Nieuw
52+
* </StatusBadge>
53+
*
54+
* // Warning met icoon
55+
* <StatusBadge variant="warning" iconStart={<Icon name="alert-triangle" size="sm" aria-hidden />}>
56+
* Let op
57+
* </StatusBadge>
58+
* ```
59+
*/
60+
export const StatusBadge = React.forwardRef<HTMLElement, StatusBadgeProps>(
61+
({ className, variant = 'neutral', iconStart, children, ...props }, ref) => {
62+
const classes = classNames(
63+
'dsn-status-badge',
64+
variant !== 'neutral' && `dsn-status-badge--${variant}`,
65+
className
66+
);
67+
68+
return (
69+
<strong ref={ref} className={classes} {...props}>
70+
{iconStart}
71+
{children}
72+
</strong>
73+
);
74+
}
75+
);
76+
77+
StatusBadge.displayName = 'StatusBadge';
Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
export * from './StatusBadge';

packages/components-react/src/index.ts

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -38,6 +38,9 @@ export * from './RadioGroup';
3838
export * from './RadioOption';
3939
export * from './OptionLabel';
4040

41+
// Display & Feedback Components
42+
export * from './StatusBadge';
43+
4144
// Form Field Components
4245
export * from './FormField';
4346
export * from './FormFieldLabel';
Lines changed: 110 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,110 @@
1+
{
2+
"dsn": {
3+
"status-badge": {
4+
"font-size": {
5+
"value": "{dsn.text.font-size.sm}",
6+
"type": "fontSize",
7+
"comment": "Status badge font size"
8+
},
9+
"line-height": {
10+
"value": "{dsn.text.line-height.sm}",
11+
"type": "lineHeight",
12+
"comment": "Status badge line height"
13+
},
14+
"border-radius": {
15+
"value": "{dsn.border.radius.sm}",
16+
"type": "dimension",
17+
"comment": "Status badge border radius"
18+
},
19+
"border-width": {
20+
"value": "{dsn.border.width.thin}",
21+
"type": "dimension",
22+
"comment": "Status badge border width"
23+
},
24+
"border-color": {
25+
"value": "{dsn.color.transparent}",
26+
"type": "color",
27+
"comment": "Status badge border color (transparent by default; visible in forced-colors / High Contrast mode)"
28+
},
29+
"text-transform": {
30+
"value": "none",
31+
"comment": "Status badge text transform (none by default; themes can override to uppercase)"
32+
},
33+
"gap": {
34+
"value": "{dsn.space.text.sm}",
35+
"type": "dimension",
36+
"comment": "Gap between icon and label"
37+
},
38+
"padding-block": {
39+
"value": "{dsn.space.block.xs}",
40+
"type": "dimension",
41+
"comment": "Vertical padding"
42+
},
43+
"padding-inline": {
44+
"value": "{dsn.space.inline.md}",
45+
"type": "dimension",
46+
"comment": "Horizontal padding"
47+
},
48+
"neutral": {
49+
"color": {
50+
"value": "{dsn.color.neutral.color-default}",
51+
"type": "color",
52+
"comment": "Text color for neutral variant"
53+
},
54+
"background-color": {
55+
"value": "{dsn.color.neutral.bg-default}",
56+
"type": "color",
57+
"comment": "Background color for neutral variant"
58+
}
59+
},
60+
"info": {
61+
"color": {
62+
"value": "{dsn.color.info.color-default}",
63+
"type": "color",
64+
"comment": "Text color for info variant"
65+
},
66+
"background-color": {
67+
"value": "{dsn.color.info.bg-default}",
68+
"type": "color",
69+
"comment": "Background color for info variant"
70+
}
71+
},
72+
"positive": {
73+
"color": {
74+
"value": "{dsn.color.positive.color-default}",
75+
"type": "color",
76+
"comment": "Text color for positive variant"
77+
},
78+
"background-color": {
79+
"value": "{dsn.color.positive.bg-default}",
80+
"type": "color",
81+
"comment": "Background color for positive variant"
82+
}
83+
},
84+
"negative": {
85+
"color": {
86+
"value": "{dsn.color.negative.color-default}",
87+
"type": "color",
88+
"comment": "Text color for negative variant"
89+
},
90+
"background-color": {
91+
"value": "{dsn.color.negative.bg-default}",
92+
"type": "color",
93+
"comment": "Background color for negative variant"
94+
}
95+
},
96+
"warning": {
97+
"color": {
98+
"value": "{dsn.color.warning.color-default}",
99+
"type": "color",
100+
"comment": "Text color for warning variant"
101+
},
102+
"background-color": {
103+
"value": "{dsn.color.warning.bg-default}",
104+
"type": "color",
105+
"comment": "Background color for warning variant"
106+
}
107+
}
108+
}
109+
}
110+
}

0 commit comments

Comments
 (0)