mithril-materialized 3.9.0 → 3.11.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
  *
@@ -8193,33 +8293,75 @@
8193
8293
  _createToast() {
8194
8294
  const toast = document.createElement('div');
8195
8295
  toast.classList.add('toast');
8196
- // Add custom classes
8197
- if (this.options.classes) {
8198
- toast.classList.add(...this.options.classes.split(' '));
8199
- }
8200
- // Set content
8296
+ // Add custom classes (prefer className over deprecated classes)
8297
+ const customClasses = this.options.className || this.options.classes;
8298
+ if (customClasses) {
8299
+ toast.classList.add(...customClasses.split(' ').filter((c) => c));
8300
+ }
8301
+ // Create content wrapper
8302
+ const contentWrapper = document.createElement('div');
8303
+ contentWrapper.style.cssText = 'display: flex; align-items: center; gap: 12px;';
8304
+ // Set message content
8305
+ const messageEl = document.createElement('div');
8306
+ messageEl.style.flex = '1';
8201
8307
  const message = this.options.html;
8202
8308
  if (typeof message === 'object' && message && 'nodeType' in message) {
8203
- toast.appendChild(message);
8309
+ messageEl.appendChild(message);
8204
8310
  }
8205
8311
  else {
8206
- toast.innerHTML = message;
8312
+ messageEl.innerHTML = message;
8313
+ }
8314
+ contentWrapper.appendChild(messageEl);
8315
+ // Add action button if provided
8316
+ if (this.options.action) {
8317
+ const action = this.options.action;
8318
+ const actionBtn = document.createElement('button');
8319
+ actionBtn.className = 'btn-flat toast-action';
8320
+ actionBtn.style.cssText = 'margin: 0; padding: 0 8px; min-width: auto; height: 36px;';
8321
+ if (action.variant === 'icon' && action.icon) {
8322
+ // Icon button variant
8323
+ actionBtn.innerHTML = `<i class="material-icons">${action.icon}</i>`;
8324
+ actionBtn.setAttribute('aria-label', action.label || action.icon);
8325
+ }
8326
+ else {
8327
+ // Flat button variant (default)
8328
+ if (action.icon) {
8329
+ actionBtn.innerHTML = `<i class="material-icons left">${action.icon}</i>${action.label || ''}`;
8330
+ }
8331
+ else {
8332
+ actionBtn.textContent = action.label || '';
8333
+ }
8334
+ }
8335
+ actionBtn.onclick = (e) => {
8336
+ e.stopPropagation();
8337
+ action.onclick();
8338
+ this.dismiss();
8339
+ };
8340
+ contentWrapper.appendChild(actionBtn);
8207
8341
  }
8342
+ toast.appendChild(contentWrapper);
8208
8343
  // Store reference
8209
8344
  toast.M_Toast = this;
8345
+ // Measure natural width BEFORE appending to avoid interference from other toasts
8346
+ // Temporarily append to body in an isolated position
8347
+ toast.style.cssText = 'position: absolute; visibility: hidden; left: -9999px; display: flex;';
8348
+ document.body.appendChild(toast);
8349
+ // Force layout and measure
8350
+ const naturalWidth = toast.offsetWidth;
8351
+ // Remove from body
8352
+ document.body.removeChild(toast);
8353
+ // Lock the width to prevent changes based on other toasts
8354
+ toast.style.cssText = `width: ${naturalWidth}px;`;
8210
8355
  // Append to container
8211
8356
  Toast._container.appendChild(toast);
8212
8357
  return toast;
8213
8358
  }
8214
8359
  _animateIn() {
8215
- // Simple CSS animation since we don't have anime.js
8216
- this.el.style.cssText = `
8217
- transform: translateY(35px);
8218
- opacity: 0;
8219
- transition: transform ${this.options.inDuration}ms cubic-bezier(0.215, 0.61, 0.355, 1),
8220
- opacity ${this.options.inDuration}ms cubic-bezier(0.215, 0.61, 0.355, 1);
8221
- `;
8222
- // Trigger animation
8360
+ // Width is already locked from _createToast
8361
+ // Set initial animation state (transitions are defined in CSS)
8362
+ this.el.style.transform = 'translateY(35px)';
8363
+ this.el.style.opacity = '0';
8364
+ // Trigger animation after a brief delay to ensure styles are applied
8223
8365
  setTimeout(() => {
8224
8366
  this.el.style.transform = 'translateY(0)';
8225
8367
  this.el.style.opacity = '1';
@@ -8243,17 +8385,34 @@
8243
8385
  }
8244
8386
  const activationDistance = this.el.offsetWidth * this.options.activationPercent;
8245
8387
  if (this.state.wasSwiped) {
8388
+ // Override transition temporarily for swipe
8246
8389
  this.el.style.transition = 'transform .05s, opacity .05s';
8247
8390
  this.el.style.transform = `translateX(${activationDistance}px)`;
8248
8391
  this.el.style.opacity = '0';
8392
+ // Reset transition after swipe animation
8393
+ setTimeout(() => {
8394
+ this.el.style.transition = `opacity ${this.options.outDuration}ms cubic-bezier(0.165, 0.84, 0.44, 1),
8395
+ max-height ${this.options.outDuration}ms cubic-bezier(0.165, 0.84, 0.44, 1),
8396
+ margin-top ${this.options.outDuration}ms cubic-bezier(0.165, 0.84, 0.44, 1),
8397
+ padding-top ${this.options.outDuration}ms cubic-bezier(0.165, 0.84, 0.44, 1),
8398
+ padding-bottom ${this.options.outDuration}ms cubic-bezier(0.165, 0.84, 0.44, 1)`;
8399
+ }, 50);
8249
8400
  }
8250
- // Animate out
8251
- this.el.style.cssText += `
8252
- transition: opacity ${this.options.outDuration}ms cubic-bezier(0.165, 0.84, 0.44, 1),
8253
- margin-top ${this.options.outDuration}ms cubic-bezier(0.165, 0.84, 0.44, 1);
8254
- opacity: 0;
8255
- margin-top: -40px;
8256
- `;
8401
+ else {
8402
+ // Set collapse transition timing
8403
+ this.el.style.transition = `opacity ${this.options.outDuration}ms cubic-bezier(0.165, 0.84, 0.44, 1),
8404
+ max-height ${this.options.outDuration}ms cubic-bezier(0.165, 0.84, 0.44, 1),
8405
+ margin-top ${this.options.outDuration}ms cubic-bezier(0.165, 0.84, 0.44, 1),
8406
+ padding-top ${this.options.outDuration}ms cubic-bezier(0.165, 0.84, 0.44, 1),
8407
+ padding-bottom ${this.options.outDuration}ms cubic-bezier(0.165, 0.84, 0.44, 1)`;
8408
+ }
8409
+ // Animate out - collapse height smoothly
8410
+ this.el.style.opacity = '0';
8411
+ this.el.style.maxHeight = '0';
8412
+ this.el.style.marginTop = '0';
8413
+ this.el.style.paddingTop = '0';
8414
+ this.el.style.paddingBottom = '0';
8415
+ this.el.style.overflow = 'hidden';
8257
8416
  setTimeout(() => {
8258
8417
  // Call completion callback
8259
8418
  if (this.options.completeCallback) {
@@ -8282,8 +8441,10 @@
8282
8441
  inDuration: 300,
8283
8442
  outDuration: 375,
8284
8443
  classes: '',
8444
+ className: '',
8285
8445
  completeCallback: undefined,
8286
8446
  activationPercent: 0.8,
8447
+ action: undefined,
8287
8448
  };
8288
8449
  Toast._onDragStart = (e) => {
8289
8450
  const target = e.target;
@@ -11035,6 +11196,7 @@
11035
11196
  exports.AnalogClock = AnalogClock;
11036
11197
  exports.AnchorItem = AnchorItem;
11037
11198
  exports.Autocomplete = Autocomplete;
11199
+ exports.Badge = Badge;
11038
11200
  exports.Breadcrumb = Breadcrumb;
11039
11201
  exports.BreadcrumbManager = BreadcrumbManager;
11040
11202
  exports.Button = Button;
package/dist/toast.d.ts CHANGED
@@ -1,4 +1,14 @@
1
1
  import { FactoryComponent } from 'mithril';
2
+ export interface ToastAction {
3
+ /** Icon name for the action button */
4
+ icon?: string;
5
+ /** Text label for the action button (if no icon provided) */
6
+ label?: string;
7
+ /** Callback function called when action is clicked */
8
+ onclick: () => void;
9
+ /** Whether to use flat button style (default) or icon button style */
10
+ variant?: 'flat' | 'icon';
11
+ }
2
12
  export interface ToastOptions {
3
13
  /** HTML content for the toast */
4
14
  html?: string;
@@ -8,12 +18,19 @@ export interface ToastOptions {
8
18
  inDuration?: number;
9
19
  /** Animation out duration in milliseconds */
10
20
  outDuration?: number;
11
- /** Additional CSS classes */
21
+ /**
22
+ * Additional CSS classes
23
+ * @deprecated Use className instead. This property will be removed in a future version.
24
+ */
12
25
  classes?: string;
26
+ /** Additional CSS classes (preferred over deprecated 'classes' property) */
27
+ className?: string;
13
28
  /** Callback function called when toast is dismissed */
14
29
  completeCallback?: () => void;
15
30
  /** Activation percentage for swipe dismissal */
16
31
  activationPercent?: number;
32
+ /** Optional action button (for simple confirmations or undo actions) */
33
+ action?: ToastAction;
17
34
  }
18
35
  export declare class Toast {
19
36
  el: HTMLElement;
@@ -3155,6 +3155,7 @@ td, th {
3155
3155
  align-items: center;
3156
3156
  justify-content: space-between;
3157
3157
  cursor: default;
3158
+ transition: transform 300ms cubic-bezier(0.215, 0.61, 0.355, 1), opacity 300ms cubic-bezier(0.215, 0.61, 0.355, 1), margin-top 300ms cubic-bezier(0.215, 0.61, 0.355, 1), max-height 300ms cubic-bezier(0.215, 0.61, 0.355, 1), padding-top 300ms cubic-bezier(0.215, 0.61, 0.355, 1), padding-bottom 300ms cubic-bezier(0.215, 0.61, 0.355, 1);
3158
3159
  }
3159
3160
  .toast .toast-action {
3160
3161
  color: #eeff41;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "mithril-materialized",
3
- "version": "3.9.0",
3
+ "version": "3.11.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
+ }
@@ -43,6 +43,14 @@
43
43
  justify-content: space-between;
44
44
  cursor: default;
45
45
 
46
+ // Smooth transitions for position changes when toasts are added/removed
47
+ transition: transform 300ms cubic-bezier(0.215, 0.61, 0.355, 1),
48
+ opacity 300ms cubic-bezier(0.215, 0.61, 0.355, 1),
49
+ margin-top 300ms cubic-bezier(0.215, 0.61, 0.355, 1),
50
+ max-height 300ms cubic-bezier(0.215, 0.61, 0.355, 1),
51
+ padding-top 300ms cubic-bezier(0.215, 0.61, 0.355, 1),
52
+ padding-bottom 300ms cubic-bezier(0.215, 0.61, 0.355, 1);
53
+
46
54
  .toast-action {
47
55
  color: variables.$toast-action-color;
48
56
  font-weight: 500;
@@ -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
+