mithril-materialized 3.8.0 → 3.10.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.umd.js CHANGED
@@ -509,6 +509,106 @@
509
509
  };
510
510
  };
511
511
 
512
+ /**
513
+ * Badge component
514
+ *
515
+ * Displays a badge anchored to a child element. Commonly used for notifications,
516
+ * counts, or status indicators. Supports flexible positioning, colors, and variants.
517
+ *
518
+ * @example
519
+ * ```typescript
520
+ * // Basic notification badge
521
+ * m(Badge, { badgeContent: 5 },
522
+ * m('button.btn', 'Messages')
523
+ * )
524
+ *
525
+ * // Dot badge on avatar
526
+ * m(Badge, {
527
+ * variant: 'dot',
528
+ * color: 'green',
529
+ * overlap: 'circular'
530
+ * },
531
+ * m('img.circle', { src: 'avatar.jpg' })
532
+ * )
533
+ * ```
534
+ */
535
+ const Badge = () => {
536
+ return {
537
+ view: ({ attrs, children }) => {
538
+ const { badgeContent, max, anchorOrigin = { vertical: 'top', horizontal: 'right' }, overlap = 'rectangular', variant = 'standard', color = 'red', colorIntensity, invisible = false, showZero = false, 'aria-label': ariaLabel, badgeClassName = '', className = '' } = attrs, params = __rest(attrs, ["badgeContent", "max", "anchorOrigin", "overlap", "variant", "color", "colorIntensity", "invisible", "showZero", 'aria-label', "badgeClassName", "className"]);
539
+ // === VALIDATION: Single child element ===
540
+ const childArray = Array.isArray(children) ? children : children ? [children] : [];
541
+ if (childArray.length === 0) {
542
+ console.warn('Badge component requires a child element');
543
+ return null;
544
+ }
545
+ if (childArray.length > 1) {
546
+ console.warn('Badge component should only wrap a single child element. Using first child only.');
547
+ }
548
+ const child = childArray[0];
549
+ // === VISIBILITY LOGIC ===
550
+ // Hide badge if:
551
+ // 1. invisible prop is true, OR
552
+ // 2. For standard variant: badgeContent is undefined/null OR (badgeContent is 0 AND !showZero)
553
+ const shouldHideBadge = invisible ||
554
+ (variant === 'standard' &&
555
+ (badgeContent === undefined ||
556
+ badgeContent === null ||
557
+ (badgeContent === 0 && !showZero)));
558
+ // === BADGE CONTENT FORMATTING ===
559
+ // Apply max capping: if badgeContent > max, show "max+"
560
+ const getDisplayContent = () => {
561
+ if (variant === 'dot')
562
+ return '';
563
+ if (typeof badgeContent === 'number' && max !== undefined && badgeContent > max) {
564
+ return `${max}+`;
565
+ }
566
+ return String(badgeContent !== null && badgeContent !== void 0 ? badgeContent : '');
567
+ };
568
+ const displayContent = getDisplayContent();
569
+ // === CSS CLASS ASSEMBLY ===
570
+ // Wrapper classes
571
+ const wrapperClasses = ['badge-wrapper', className].filter(Boolean).join(' ').trim() || undefined;
572
+ // Badge element classes - using m-badge prefix to avoid Materialize conflicts
573
+ const positionClass = `m-badge--${anchorOrigin.vertical}-${anchorOrigin.horizontal}`;
574
+ const badgeClasses = [
575
+ 'm-badge',
576
+ `m-badge--${variant}`,
577
+ positionClass,
578
+ `m-badge--${overlap}`,
579
+ `m-badge--${color}`,
580
+ colorIntensity ? `m-badge--${colorIntensity}` : '',
581
+ shouldHideBadge ? 'm-badge--invisible' : '',
582
+ badgeClassName,
583
+ ]
584
+ .filter(Boolean)
585
+ .join(' ')
586
+ .trim();
587
+ // === ARIA ATTRIBUTES ===
588
+ const badgeAriaLabel = ariaLabel ||
589
+ (variant === 'dot'
590
+ ? 'notification indicator'
591
+ : displayContent
592
+ ? `${displayContent} notifications`
593
+ : 'notification badge');
594
+ // === RENDER ===
595
+ return m('.badge-wrapper', Object.assign(Object.assign({}, params), { className: wrapperClasses }), [
596
+ // Child element
597
+ child,
598
+ // Badge element - only render if not hidden
599
+ !shouldHideBadge
600
+ ? m('span', {
601
+ className: badgeClasses,
602
+ 'aria-label': badgeAriaLabel,
603
+ role: 'status',
604
+ 'aria-live': 'polite',
605
+ }, variant === 'standard' ? displayContent : null)
606
+ : null,
607
+ ]);
608
+ },
609
+ };
610
+ };
611
+
512
612
  /**
513
613
  * A simple material icon, defined by its icon name.
514
614
  *
@@ -10835,6 +10935,182 @@
10835
10935
  };
10836
10936
  };
10837
10937
 
10938
+ /** Size dimensions in pixels */
10939
+ const SIZE_MAP = {
10940
+ small: 36,
10941
+ medium: 50,
10942
+ large: 64,
10943
+ };
10944
+ /** Stroke width in pixels */
10945
+ const STROKE_WIDTH = 3;
10946
+ /** Create a CircularProgress component */
10947
+ const CircularProgress = () => {
10948
+ const state = {
10949
+ id: uniqueId(),
10950
+ };
10951
+ /**
10952
+ * Calculate SVG stroke properties for determinate progress
10953
+ */
10954
+ const calculateStrokeProperties = (size, value, max) => {
10955
+ const radius = (size - STROKE_WIDTH) / 2;
10956
+ const circumference = 2 * Math.PI * radius;
10957
+ const percentage = Math.min(100, Math.max(0, (value / max) * 100));
10958
+ const strokeDashoffset = circumference - (percentage / 100) * circumference;
10959
+ return {
10960
+ radius,
10961
+ circumference,
10962
+ strokeDashoffset,
10963
+ percentage,
10964
+ };
10965
+ };
10966
+ /**
10967
+ * Get size class name
10968
+ */
10969
+ const getSizeClass = (size = 'medium') => {
10970
+ return `circular-progress--${size}`;
10971
+ };
10972
+ /**
10973
+ * Get color class name
10974
+ */
10975
+ const getColorClass = (color, intensity) => {
10976
+ if (!color)
10977
+ return '';
10978
+ return intensity ? `circular-progress--${color} circular-progress--${intensity}` : `circular-progress--${color}`;
10979
+ };
10980
+ return {
10981
+ view: ({ attrs }) => {
10982
+ const { mode = 'indeterminate', value = 0, max = 100, size = 'medium', color = 'teal', colorIntensity, label, showPercentage = false, className = '', style = {}, id = state.id, 'aria-label': ariaLabel, 'aria-valuemin': ariaValueMin = 0, 'aria-valuemax': ariaValueMax = max, 'aria-valuenow': ariaValueNow, 'aria-valuetext': ariaValueText } = attrs, params = __rest(attrs, ["mode", "value", "max", "size", "color", "colorIntensity", "label", "showPercentage", "className", "style", "id", 'aria-label', 'aria-valuemin', 'aria-valuemax', 'aria-valuenow', 'aria-valuetext']);
10983
+ const isDeterminate = mode === 'determinate';
10984
+ const sizePixels = SIZE_MAP[size];
10985
+ const { radius, circumference, strokeDashoffset, percentage } = isDeterminate
10986
+ ? calculateStrokeProperties(sizePixels, value, max)
10987
+ : { radius: 0, circumference: 0, strokeDashoffset: 0, percentage: 0 };
10988
+ // Determine label content
10989
+ const labelContent = label !== undefined ? label : showPercentage && isDeterminate ? `${Math.round(percentage)}%` : '';
10990
+ // Build class names
10991
+ const classNames = [
10992
+ 'circular-progress',
10993
+ getSizeClass(size),
10994
+ getColorClass(color, colorIntensity),
10995
+ `circular-progress--${mode}`,
10996
+ className,
10997
+ ]
10998
+ .filter(Boolean)
10999
+ .join(' ');
11000
+ // ARIA attributes
11001
+ const ariaAttrs = isDeterminate
11002
+ ? {
11003
+ 'aria-valuenow': ariaValueNow !== undefined ? ariaValueNow : value,
11004
+ 'aria-valuemin': ariaValueMin,
11005
+ 'aria-valuemax': ariaValueMax,
11006
+ 'aria-valuetext': ariaValueText || `${Math.round(percentage)}%`,
11007
+ }
11008
+ : {
11009
+ 'aria-valuetext': ariaValueText || label || 'Loading',
11010
+ };
11011
+ return m('.circular-progress', Object.assign(Object.assign(Object.assign({}, params), { className: classNames, style: Object.assign({ width: `${sizePixels}px`, height: `${sizePixels}px` }, style), id, role: 'progressbar', 'aria-label': ariaLabel || (isDeterminate ? `Progress: ${Math.round(percentage)}%` : 'Loading') }), ariaAttrs), [
11012
+ // SVG circle
11013
+ m('svg.circular-progress__svg', {
11014
+ viewBox: `0 0 ${sizePixels} ${sizePixels}`,
11015
+ xmlns: 'http://www.w3.org/2000/svg',
11016
+ }, [
11017
+ // Background track circle
11018
+ m('circle.circular-progress__circle.circular-progress__circle--track', {
11019
+ cx: sizePixels / 2,
11020
+ cy: sizePixels / 2,
11021
+ r: radius || (sizePixels - STROKE_WIDTH) / 2,
11022
+ fill: 'none',
11023
+ stroke: 'currentColor',
11024
+ 'stroke-width': STROKE_WIDTH,
11025
+ }),
11026
+ // Progress indicator circle
11027
+ m('circle.circular-progress__circle.circular-progress__circle--indicator', {
11028
+ cx: sizePixels / 2,
11029
+ cy: sizePixels / 2,
11030
+ r: radius || (sizePixels - STROKE_WIDTH) / 2,
11031
+ fill: 'none',
11032
+ stroke: 'currentColor',
11033
+ 'stroke-width': STROKE_WIDTH,
11034
+ 'stroke-dasharray': isDeterminate ? circumference : undefined,
11035
+ 'stroke-dashoffset': isDeterminate ? strokeDashoffset : undefined,
11036
+ 'stroke-linecap': 'round',
11037
+ }),
11038
+ ]),
11039
+ // Label inside circle
11040
+ labelContent &&
11041
+ m('.circular-progress__label', {
11042
+ 'aria-hidden': 'true',
11043
+ }, labelContent),
11044
+ ]);
11045
+ },
11046
+ };
11047
+ };
11048
+
11049
+ /** Create a LinearProgress component */
11050
+ const LinearProgress = () => {
11051
+ const state = {
11052
+ id: uniqueId(),
11053
+ };
11054
+ /**
11055
+ * Get size class name
11056
+ */
11057
+ const getSizeClass = (size = 'medium') => {
11058
+ return `linear-progress__track--${size}`;
11059
+ };
11060
+ /**
11061
+ * Get color class name
11062
+ */
11063
+ const getColorClass = (color, intensity) => {
11064
+ if (!color)
11065
+ return '';
11066
+ return intensity ? `linear-progress--${color} linear-progress--${intensity}` : `linear-progress--${color}`;
11067
+ };
11068
+ return {
11069
+ view: ({ attrs }) => {
11070
+ const { mode = 'indeterminate', value = 0, max = 100, size = 'medium', color = 'teal', colorIntensity, label, showPercentage = false, className = '', style = {}, id = state.id, 'aria-label': ariaLabel, 'aria-valuemin': ariaValueMin = 0, 'aria-valuemax': ariaValueMax = max, 'aria-valuenow': ariaValueNow, 'aria-valuetext': ariaValueText } = attrs, params = __rest(attrs, ["mode", "value", "max", "size", "color", "colorIntensity", "label", "showPercentage", "className", "style", "id", 'aria-label', 'aria-valuemin', 'aria-valuemax', 'aria-valuenow', 'aria-valuetext']);
11071
+ const isDeterminate = mode === 'determinate';
11072
+ const percentage = Math.min(100, Math.max(0, (value / max) * 100));
11073
+ // Determine label content
11074
+ const labelContent = label !== undefined ? label : showPercentage && isDeterminate ? `${Math.round(percentage)}%` : '';
11075
+ // Build class names
11076
+ const classNames = [
11077
+ 'linear-progress',
11078
+ getColorClass(color, colorIntensity),
11079
+ className,
11080
+ ]
11081
+ .filter(Boolean)
11082
+ .join(' ');
11083
+ // ARIA attributes
11084
+ const ariaAttrs = isDeterminate
11085
+ ? {
11086
+ 'aria-valuenow': ariaValueNow !== undefined ? ariaValueNow : value,
11087
+ 'aria-valuemin': ariaValueMin,
11088
+ 'aria-valuemax': ariaValueMax,
11089
+ 'aria-valuetext': ariaValueText || `${Math.round(percentage)}%`,
11090
+ }
11091
+ : {
11092
+ 'aria-valuetext': ariaValueText || label || 'Loading',
11093
+ };
11094
+ return m('.linear-progress', Object.assign(Object.assign({}, params), { className: classNames, style,
11095
+ id }), [
11096
+ // Progress track container
11097
+ m('.linear-progress__track', Object.assign({ className: `linear-progress__track ${getSizeClass(size)}`, role: 'progressbar', 'aria-label': ariaLabel || (isDeterminate ? `Progress: ${Math.round(percentage)}%` : 'Loading') }, ariaAttrs), [
11098
+ // Progress bar
11099
+ m('.linear-progress__bar', {
11100
+ className: `linear-progress__bar ${isDeterminate ? '' : 'linear-progress__bar--indeterminate'}`,
11101
+ style: isDeterminate ? { width: `${percentage}%` } : undefined,
11102
+ }),
11103
+ ]),
11104
+ // Label at the end (right side)
11105
+ labelContent &&
11106
+ m('.linear-progress__label', {
11107
+ 'aria-hidden': 'true',
11108
+ }, labelContent),
11109
+ ]);
11110
+ },
11111
+ };
11112
+ };
11113
+
10838
11114
  /**
10839
11115
  * @fileoverview Core TypeScript utility types for mithril-materialized library
10840
11116
  * These types improve type safety and developer experience across all components
@@ -10859,6 +11135,7 @@
10859
11135
  exports.AnalogClock = AnalogClock;
10860
11136
  exports.AnchorItem = AnchorItem;
10861
11137
  exports.Autocomplete = Autocomplete;
11138
+ exports.Badge = Badge;
10862
11139
  exports.Breadcrumb = Breadcrumb;
10863
11140
  exports.BreadcrumbManager = BreadcrumbManager;
10864
11141
  exports.Button = Button;
@@ -10866,6 +11143,7 @@
10866
11143
  exports.Carousel = Carousel;
10867
11144
  exports.CharacterCounter = CharacterCounter;
10868
11145
  exports.Chips = Chips;
11146
+ exports.CircularProgress = CircularProgress;
10869
11147
  exports.CodeBlock = CodeBlock;
10870
11148
  exports.Collapsible = Collapsible;
10871
11149
  exports.CollapsibleItem = CollapsibleItem;
@@ -10888,6 +11166,7 @@
10888
11166
  exports.InputCheckbox = InputCheckbox;
10889
11167
  exports.Label = Label;
10890
11168
  exports.LargeButton = LargeButton;
11169
+ exports.LinearProgress = LinearProgress;
10891
11170
  exports.ListItem = ListItem;
10892
11171
  exports.Mandatory = Mandatory;
10893
11172
  exports.Masonry = Masonry;
@@ -0,0 +1,40 @@
1
+ import { FactoryComponent, Attributes } from 'mithril';
2
+ import { MaterialColor, ColorIntensity } from './types';
3
+ import { ProgressMode, ProgressSize } from './circular-progress';
4
+ /** LinearProgress component attributes */
5
+ export interface LinearProgressAttrs extends Attributes {
6
+ /** Progress mode (default: 'indeterminate') */
7
+ mode?: ProgressMode;
8
+ /** Current progress value (0-100) for determinate mode */
9
+ value?: number;
10
+ /** Maximum progress value (default: 100) */
11
+ max?: number;
12
+ /** Size variant (default: 'medium') */
13
+ size?: ProgressSize;
14
+ /** Materialize color (default: 'teal') */
15
+ color?: MaterialColor;
16
+ /** Color intensity modifier */
17
+ colorIntensity?: ColorIntensity;
18
+ /** Label to display at the end (right side) */
19
+ label?: string | number;
20
+ /** Auto-show percentage at the end for determinate mode (default: false) */
21
+ showPercentage?: boolean;
22
+ /** Additional CSS class names */
23
+ className?: string;
24
+ /** Additional CSS styles */
25
+ style?: any;
26
+ /** HTML ID for the component */
27
+ id?: string;
28
+ /** ARIA label for accessibility */
29
+ 'aria-label'?: string;
30
+ /** ARIA valuemin (default: 0) */
31
+ 'aria-valuemin'?: number;
32
+ /** ARIA valuemax (default: 100) */
33
+ 'aria-valuemax'?: number;
34
+ /** ARIA valuenow (current value) */
35
+ 'aria-valuenow'?: number;
36
+ /** ARIA valuetext (custom text description) */
37
+ 'aria-valuetext'?: string;
38
+ }
39
+ /** Create a LinearProgress component */
40
+ export declare const LinearProgress: FactoryComponent<LinearProgressAttrs>;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "mithril-materialized",
3
- "version": "3.8.0",
3
+ "version": "3.10.0",
4
4
  "description": "A materialize library for mithril.",
5
5
  "main": "dist/index.js",
6
6
  "module": "dist/index.esm.js",
@@ -0,0 +1,203 @@
1
+ @use 'sass:color';
2
+ @use "variables";
3
+ @use "color-variables";
4
+
5
+ /* Badge Component
6
+ ========================================================================== */
7
+
8
+ // Badge component variables
9
+ $badge-size-standard: 20px !default;
10
+ $badge-size-dot: 8px !default;
11
+ $badge-font-size: 12px !default;
12
+ $badge-font-weight: 500 !default;
13
+ $badge-default-color: color-variables.color("red", "base") !default;
14
+ $badge-text-color: #fff !default;
15
+ $badge-border-radius: 10px !default;
16
+ $badge-dot-border-radius: 50% !default;
17
+
18
+ // Wrapper - relative positioning container
19
+ .badge-wrapper {
20
+ position: relative;
21
+ display: inline-flex;
22
+ vertical-align: middle;
23
+ flex-shrink: 0;
24
+ }
25
+
26
+ // Badge element - using .m-badge to avoid conflicts with Materialize's span.badge
27
+ .m-badge {
28
+ position: absolute;
29
+ display: flex;
30
+ align-items: center;
31
+ justify-content: center;
32
+ box-sizing: border-box;
33
+ font-family: Roboto, sans-serif;
34
+ font-weight: $badge-font-weight;
35
+ line-height: 1;
36
+ white-space: nowrap;
37
+ text-align: center;
38
+ border-radius: $badge-border-radius;
39
+ background-color: $badge-default-color;
40
+ color: $badge-text-color;
41
+ z-index: 1;
42
+ transition: transform 225ms cubic-bezier(0.4, 0, 0.2, 1);
43
+
44
+ // Standard variant (shows content)
45
+ &.m-badge--standard {
46
+ min-width: $badge-size-standard;
47
+ height: $badge-size-standard;
48
+ padding: 0 6px;
49
+ font-size: $badge-font-size;
50
+ }
51
+
52
+ // Dot variant (minimal indicator)
53
+ &.m-badge--dot {
54
+ width: $badge-size-dot;
55
+ height: $badge-size-dot;
56
+ min-width: $badge-size-dot;
57
+ padding: 0;
58
+ border-radius: $badge-dot-border-radius;
59
+ }
60
+
61
+ // Invisible state
62
+ &.m-badge--invisible {
63
+ transform: scale(0);
64
+ opacity: 0;
65
+ }
66
+
67
+ // === POSITIONING: Top-Right (default) ===
68
+ &.m-badge--top-right {
69
+ top: 0;
70
+ right: 0;
71
+ transform: scale(1) translate(50%, -50%);
72
+ transform-origin: 100% 0%;
73
+
74
+ &.m-badge--rectangular {
75
+ transform: scale(1) translate(50%, -50%);
76
+ }
77
+
78
+ &.m-badge--circular {
79
+ transform: scale(1) translate(30%, -30%);
80
+ }
81
+
82
+ &.m-badge--invisible {
83
+ transform: scale(0) translate(50%, -50%);
84
+ }
85
+ }
86
+
87
+ // === POSITIONING: Top-Left ===
88
+ &.m-badge--top-left {
89
+ top: 0;
90
+ left: 0;
91
+ transform: scale(1) translate(-50%, -50%);
92
+ transform-origin: 0% 0%;
93
+
94
+ &.m-badge--rectangular {
95
+ transform: scale(1) translate(-50%, -50%);
96
+ }
97
+
98
+ &.m-badge--circular {
99
+ transform: scale(1) translate(-30%, -30%);
100
+ }
101
+
102
+ &.m-badge--invisible {
103
+ transform: scale(0) translate(-50%, -50%);
104
+ }
105
+ }
106
+
107
+ // === POSITIONING: Bottom-Right ===
108
+ &.m-badge--bottom-right {
109
+ bottom: 0;
110
+ right: 0;
111
+ transform: scale(1) translate(50%, 50%);
112
+ transform-origin: 100% 100%;
113
+
114
+ &.m-badge--rectangular {
115
+ transform: scale(1) translate(50%, 50%);
116
+ }
117
+
118
+ &.m-badge--circular {
119
+ transform: scale(1) translate(30%, 30%);
120
+ }
121
+
122
+ &.m-badge--invisible {
123
+ transform: scale(0) translate(50%, 50%);
124
+ }
125
+ }
126
+
127
+ // === POSITIONING: Bottom-Left ===
128
+ &.m-badge--bottom-left {
129
+ bottom: 0;
130
+ left: 0;
131
+ transform: scale(1) translate(-50%, 50%);
132
+ transform-origin: 0% 100%;
133
+
134
+ &.m-badge--rectangular {
135
+ transform: scale(1) translate(-50%, 50%);
136
+ }
137
+
138
+ &.m-badge--circular {
139
+ transform: scale(1) translate(-30%, 30%);
140
+ }
141
+
142
+ &.m-badge--invisible {
143
+ transform: scale(0) translate(-50%, 50%);
144
+ }
145
+ }
146
+ }
147
+
148
+ // === COLOR VARIANTS ===
149
+ // Generate color classes for all MaterialColor options
150
+ .m-badge--red { background-color: color-variables.color("red", "base"); }
151
+ .m-badge--pink { background-color: color-variables.color("pink", "base"); }
152
+ .m-badge--purple { background-color: color-variables.color("purple", "base"); }
153
+ .m-badge--deep-purple { background-color: color-variables.color("deep-purple", "base"); }
154
+ .m-badge--indigo { background-color: color-variables.color("indigo", "base"); }
155
+ .m-badge--blue { background-color: color-variables.color("blue", "base"); }
156
+ .m-badge--light-blue { background-color: color-variables.color("light-blue", "base"); }
157
+ .m-badge--cyan { background-color: color-variables.color("cyan", "base"); }
158
+ .m-badge--teal { background-color: color-variables.color("teal", "base"); }
159
+ .m-badge--green { background-color: color-variables.color("green", "base"); }
160
+ .m-badge--light-green { background-color: color-variables.color("light-green", "base"); }
161
+ .m-badge--lime { background-color: color-variables.color("lime", "base"); }
162
+ .m-badge--yellow {
163
+ background-color: color-variables.color("yellow", "base");
164
+ color: #000; // Dark text for light background
165
+ }
166
+ .m-badge--amber { background-color: color-variables.color("amber", "base"); }
167
+ .m-badge--orange { background-color: color-variables.color("orange", "base"); }
168
+ .m-badge--deep-orange { background-color: color-variables.color("deep-orange", "base"); }
169
+ .m-badge--brown { background-color: color-variables.color("brown", "base"); }
170
+ .m-badge--grey { background-color: color-variables.color("grey", "base"); }
171
+ .m-badge--blue-grey { background-color: color-variables.color("blue-grey", "base"); }
172
+
173
+ // === COLOR INTENSITY MODIFIERS ===
174
+ // Use CSS filters for intensity adjustments (matching CircularProgress pattern)
175
+ .m-badge--lighten-5 { filter: brightness(1.4); }
176
+ .m-badge--lighten-4 { filter: brightness(1.3); }
177
+ .m-badge--lighten-3 { filter: brightness(1.2); }
178
+ .m-badge--lighten-2 { filter: brightness(1.1); }
179
+ .m-badge--lighten-1 { filter: brightness(1.05); }
180
+ .m-badge--darken-1 { filter: brightness(0.95); }
181
+ .m-badge--darken-2 { filter: brightness(0.9); }
182
+ .m-badge--darken-3 { filter: brightness(0.8); }
183
+ .m-badge--darken-4 { filter: brightness(0.7); }
184
+
185
+ // === ACCESSIBILITY ===
186
+ // Ensure badge is not announced multiple times by screen readers
187
+ .m-badge[role="status"] {
188
+ // Screen reader will announce changes due to aria-live="polite"
189
+ }
190
+
191
+ // === REDUCED MOTION ===
192
+ @media (prefers-reduced-motion: reduce) {
193
+ .m-badge {
194
+ transition: none;
195
+ }
196
+ }
197
+
198
+ // === HIGH CONTRAST MODE ===
199
+ @media (prefers-contrast: high) {
200
+ .m-badge {
201
+ border: 2px solid currentColor;
202
+ }
203
+ }