mithril-materialized 3.9.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
  *
@@ -11035,6 +11135,7 @@
11035
11135
  exports.AnalogClock = AnalogClock;
11036
11136
  exports.AnchorItem = AnchorItem;
11037
11137
  exports.Autocomplete = Autocomplete;
11138
+ exports.Badge = Badge;
11038
11139
  exports.Breadcrumb = Breadcrumb;
11039
11140
  exports.BreadcrumbManager = BreadcrumbManager;
11040
11141
  exports.Button = Button;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "mithril-materialized",
3
- "version": "3.9.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
+ }
@@ -56,3 +56,5 @@
56
56
  @use 'components/toggle-group';
57
57
  @use 'components/circular-progress';
58
58
  @use 'components/linear-progress';
59
+ @use 'components/badge-component';
60
+