mithril-materialized 3.5.3 → 3.5.5

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
@@ -7814,6 +7814,87 @@
7814
7814
  };
7815
7815
  };
7816
7816
 
7817
+ // List of MaterialIcon SVG icons that are available
7818
+ const materialIconSvgNames = [
7819
+ 'caret',
7820
+ 'close',
7821
+ 'chevron',
7822
+ 'chevron_left',
7823
+ 'chevron_right',
7824
+ 'menu',
7825
+ 'expand',
7826
+ 'collapse',
7827
+ 'check',
7828
+ 'radio_checked',
7829
+ 'radio_unchecked',
7830
+ 'light_mode',
7831
+ 'dark_mode',
7832
+ ];
7833
+ /**
7834
+ * Helper function to render icons based on IconDefinition type
7835
+ */
7836
+ const renderIcon = (icon, style) => {
7837
+ if (!icon)
7838
+ return null;
7839
+ if (typeof icon === 'string') {
7840
+ // Check if this is a MaterialIcon SVG name
7841
+ if (materialIconSvgNames.includes(icon)) {
7842
+ return m(MaterialIcon, { name: icon, style });
7843
+ }
7844
+ // Fall back to Material Icons font for other icon names
7845
+ return m('i.material-icons', { style }, icon);
7846
+ }
7847
+ if (icon.type === 'svg') {
7848
+ // Inline SVG
7849
+ return m.trust(icon.content);
7850
+ }
7851
+ if (icon.type === 'image') {
7852
+ // Image URL
7853
+ return m('img', {
7854
+ src: icon.content,
7855
+ style: Object.assign(Object.assign({}, style), { width: '24px', height: '24px', objectFit: 'contain' }),
7856
+ });
7857
+ }
7858
+ return null;
7859
+ };
7860
+ /**
7861
+ * Sidenav Header/Footer Item Component
7862
+ */
7863
+ const SidenavHeaderFooterItem = () => {
7864
+ return {
7865
+ view: ({ attrs }) => {
7866
+ const { text, icon, onclick, href, className = '', _isExpanded = true, _position = 'left' } = attrs;
7867
+ const isRightAligned = _position === 'right';
7868
+ const handleClick = (e) => {
7869
+ if (onclick) {
7870
+ e.preventDefault();
7871
+ onclick(e);
7872
+ }
7873
+ };
7874
+ const content = isRightAligned
7875
+ ? [
7876
+ _isExpanded &&
7877
+ m('span.sidenav-item-text', { style: { flex: '1', 'text-align': 'left', 'margin-right': '8px' } }, text),
7878
+ renderIcon(icon, { 'min-width': '24px', width: '24px' }),
7879
+ ]
7880
+ : [
7881
+ renderIcon(icon, { 'min-width': '24px', width: '24px' }),
7882
+ _isExpanded && m('span.sidenav-item-text', { style: { 'margin-left': '8px', flex: '1' } }, text),
7883
+ ];
7884
+ const linkStyle = {
7885
+ display: 'flex',
7886
+ 'align-items': 'center',
7887
+ padding: _isExpanded ? '12px 16px' : '12px 18px',
7888
+ 'justify-content': _isExpanded ? (isRightAligned ? 'flex-end' : 'flex-start') : 'center',
7889
+ };
7890
+ return m('li', { class: className }, m('a', {
7891
+ href: href || '#!',
7892
+ onclick: handleClick,
7893
+ style: linkStyle,
7894
+ }, content));
7895
+ },
7896
+ };
7897
+ };
7817
7898
  /**
7818
7899
  * Sidenav Component
7819
7900
  * A responsive navigation drawer that slides in from the side
@@ -7936,6 +8017,9 @@
7936
8017
  name: 'menu',
7937
8018
  style: { width: '24px', height: '24px' },
7938
8019
  })),
8020
+ // Header item (if provided, appears before expand/collapse toggle)
8021
+ attrs.header &&
8022
+ m(SidenavHeaderFooterItem, Object.assign(Object.assign({}, attrs.header), { _isExpanded: isExpanded, _position: position })),
7939
8023
  // Expand/collapse toggle button (if expandable, right below hamburger)
7940
8024
  expandable &&
7941
8025
  m('li.sidenav-expand-toggle', {
@@ -7950,8 +8034,12 @@
7950
8034
  onclick: () => toggleExpanded(attrs),
7951
8035
  }, m(MaterialIcon, {
7952
8036
  name: position === 'right'
7953
- ? (isExpanded ? 'chevron_right' : 'chevron_left')
7954
- : (isExpanded ? 'chevron_left' : 'chevron_right'),
8037
+ ? isExpanded
8038
+ ? 'chevron_right'
8039
+ : 'chevron_left'
8040
+ : isExpanded
8041
+ ? 'chevron_left'
8042
+ : 'chevron_right',
7955
8043
  style: { width: '24px', height: '24px' },
7956
8044
  })),
7957
8045
  // Children (menu items) - inject internal props
@@ -7964,6 +8052,9 @@
7964
8052
  return child;
7965
8053
  })
7966
8054
  : children,
8055
+ // Footer item (if provided, appears at the bottom)
8056
+ attrs.footer &&
8057
+ m(SidenavHeaderFooterItem, Object.assign(Object.assign({}, attrs.footer), { _isExpanded: isExpanded, _position: position, className: 'sidenav-footer-item' })),
7967
8058
  ]),
7968
8059
  ];
7969
8060
  },
@@ -7982,43 +8073,42 @@
7982
8073
  }
7983
8074
  };
7984
8075
  const isRightAligned = position === 'right';
8076
+ // Render indicator icon for checkbox/radio modes
8077
+ const indicatorIcon = mode !== 'none'
8078
+ ? m(MaterialIcon, {
8079
+ name: mode === 'checkbox' ? (selected ? 'check' : 'close') : selected ? 'radio_checked' : 'radio_unchecked',
8080
+ style: {
8081
+ width: '18px',
8082
+ height: '18px',
8083
+ opacity: mode === 'checkbox' && !selected ? '0.3' : '1',
8084
+ },
8085
+ })
8086
+ : null;
7985
8087
  const submenuContent = isRightAligned
7986
8088
  ? [
7987
8089
  // Right-aligned: text on left, icons on right
7988
- isExpanded && m('span', { style: { 'flex': '1', 'text-align': 'left' } }, text),
7989
- icon && isExpanded && m('i.material-icons', { style: { 'font-size': '18px' } }, icon),
7990
- m(MaterialIcon, {
7991
- name: mode === 'checkbox' ? (selected ? 'check' : 'close') : selected ? 'radio_checked' : 'radio_unchecked',
7992
- style: {
7993
- width: '18px',
7994
- height: '18px',
7995
- opacity: mode === 'checkbox' && !selected ? '0.3' : '1',
7996
- },
7997
- }),
8090
+ isExpanded && m('span', { style: { flex: '1', 'text-align': 'left' } }, text),
8091
+ icon && isExpanded && renderIcon(icon, { 'font-size': '18px' }),
8092
+ indicatorIcon,
7998
8093
  ]
7999
8094
  : [
8000
8095
  // Left-aligned: indicator on left, text and icon on right
8001
- m(MaterialIcon, {
8002
- name: mode === 'checkbox' ? (selected ? 'check' : 'close') : selected ? 'radio_checked' : 'radio_unchecked',
8003
- style: {
8004
- width: '18px',
8005
- height: '18px',
8006
- opacity: mode === 'checkbox' && !selected ? '0.3' : '1',
8007
- },
8008
- }),
8009
- icon && isExpanded && m('i.material-icons', { style: { 'font-size': '18px', 'margin-left': '8px' } }, icon),
8010
- isExpanded && m('span', { style: { 'margin-left': icon ? '8px' : '8px' } }, text),
8096
+ indicatorIcon,
8097
+ icon && isExpanded && renderIcon(icon, { 'font-size': '18px', 'margin-left': indicatorIcon ? '8px' : '0' }),
8098
+ isExpanded && m('span', { style: { 'margin-left': icon || indicatorIcon ? '8px' : '0' } }, text),
8011
8099
  ];
8012
8100
  return m('li.sidenav-subitem', {
8013
8101
  class: selected ? 'selected' : '',
8014
8102
  style: {
8015
- padding: isExpanded ? '8px 16px 8px 48px' : '8px 16px',
8103
+ padding: isExpanded ? '0 16px 0 48px' : '0 16px',
8016
8104
  cursor: 'pointer',
8017
8105
  display: 'flex',
8018
8106
  'align-items': 'center',
8019
8107
  gap: '8px',
8020
8108
  'font-size': '0.9em',
8021
8109
  'justify-content': isRightAligned ? 'space-between' : 'flex-start',
8110
+ height: '48px',
8111
+ 'min-height': '48px',
8022
8112
  },
8023
8113
  onclick: handleClick,
8024
8114
  }, submenuContent);
@@ -8041,20 +8131,16 @@
8041
8131
  return m('li.subheader', text || children);
8042
8132
  }
8043
8133
  const hasSubmenu = submenu && submenu.length > 0;
8044
- const itemClasses = [
8045
- active ? 'active' : '',
8046
- disabled ? 'disabled' : '',
8047
- hasSubmenu ? 'has-submenu' : '',
8048
- className,
8049
- ]
8134
+ const itemClasses = [active ? 'active' : '', disabled ? 'disabled' : '', hasSubmenu ? 'has-submenu' : '', className]
8050
8135
  .filter(Boolean)
8051
8136
  .join(' ') || undefined;
8052
8137
  const handleMainClick = (e) => {
8053
- e.preventDefault();
8054
8138
  if (hasSubmenu) {
8139
+ e.preventDefault();
8055
8140
  isSubmenuOpen = active ? !isSubmenuOpen : true;
8056
8141
  }
8057
8142
  if (onclick && !disabled) {
8143
+ e.preventDefault();
8058
8144
  onclick(e);
8059
8145
  }
8060
8146
  };
@@ -8067,13 +8153,14 @@
8067
8153
  const content = isRightAligned
8068
8154
  ? [
8069
8155
  // Right-aligned: text on left, icon on right
8070
- isExpanded && m('span.sidenav-item-text', { style: { 'flex': '1', 'text-align': 'left', 'margin-right': '8px' } }, text || children),
8071
- m('i.material-icons', { style: { 'min-width': '24px', 'width': '24px' } }, icon || ''),
8156
+ isExpanded &&
8157
+ m('span.sidenav-item-text', { style: { flex: '1', 'text-align': 'left', 'margin-right': '8px' } }, text || children),
8158
+ renderIcon(icon, { 'min-width': '24px', width: '24px' }),
8072
8159
  ]
8073
8160
  : [
8074
8161
  // Left-aligned: icon on left, text on right
8075
- m('i.material-icons', { style: { 'min-width': '24px', 'width': '24px' } }, icon || ''),
8076
- isExpanded && m('span.sidenav-item-text', { style: { 'margin-left': '8px', 'flex': '1' } }, text || children),
8162
+ renderIcon(icon, { 'min-width': '24px', width: '24px' }),
8163
+ isExpanded && m('span.sidenav-item-text', { style: { 'margin-left': '8px', flex: '1' } }, text || children),
8077
8164
  ];
8078
8165
  const linkStyle = {
8079
8166
  display: 'flex',
package/dist/sidenav.d.ts CHANGED
@@ -1,9 +1,14 @@
1
1
  import { FactoryComponent, Attributes } from 'mithril';
2
+ /** Icon definition - supports material icon name, inline SVG, or image URL */
3
+ export type IconDefinition = string | {
4
+ type: 'svg' | 'image';
5
+ content: string;
6
+ };
2
7
  export interface NavbarSubItemAttrs {
3
8
  /** Text content of the submenu item */
4
9
  text: string;
5
- /** Optional icon name */
6
- icon?: string;
10
+ /** Optional icon - material icon name, SVG object, or image object */
11
+ icon?: IconDefinition;
7
12
  /** Whether this submenu item is selected */
8
13
  selected?: boolean;
9
14
  /** Value for the submenu item */
@@ -46,12 +51,16 @@ export interface SidenavAttrs extends Attributes {
46
51
  isExpanded?: boolean;
47
52
  /** Callback when expand state changes */
48
53
  onExpandChange?: (expanded: boolean) => void;
54
+ /** Header item displayed before expand/collapse toggle */
55
+ header?: SidenavItemAttrs;
56
+ /** Footer item displayed at the bottom of the sidenav */
57
+ footer?: SidenavItemAttrs;
49
58
  }
50
59
  export interface SidenavItemAttrs {
51
60
  /** Text content of the item */
52
61
  text?: string;
53
- /** Icon name (material icons) */
54
- icon?: string;
62
+ /** Icon - material icon name, SVG object, or image object */
63
+ icon?: IconDefinition;
55
64
  /** Whether this item is active */
56
65
  active?: boolean;
57
66
  /** Whether this item is disabled */
@@ -68,8 +77,8 @@ export interface SidenavItemAttrs {
68
77
  subheader?: boolean;
69
78
  /** Submenu items */
70
79
  submenu?: NavbarSubItemAttrs[];
71
- /** Submenu selection mode */
72
- submenuMode?: 'checkbox' | 'radio';
80
+ /** Submenu selection mode - 'checkbox' for multi-select, 'radio' for single-select, 'none' for no indicators */
81
+ submenuMode?: 'checkbox' | 'radio' | 'none';
73
82
  /** @internal - Whether the sidenav is expanded (passed from parent) */
74
83
  _isExpanded?: boolean;
75
84
  /** @internal - Position of the sidenav (passed from parent) */
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "mithril-materialized",
3
- "version": "3.5.3",
3
+ "version": "3.5.5",
4
4
  "description": "A materialize library for mithril.",
5
5
  "main": "dist/index.js",
6
6
  "module": "dist/index.esm.js",
@@ -75,10 +75,10 @@
75
75
  "mithril": "^2.3.7"
76
76
  },
77
77
  "devDependencies": {
78
- "@playwright/test": "^1.55.0",
79
- "@rollup/plugin-typescript": "^12.1.4",
78
+ "@playwright/test": "^1.56.1",
79
+ "@rollup/plugin-typescript": "^12.3.0",
80
80
  "@testing-library/dom": "^10.4.1",
81
- "@testing-library/jest-dom": "^6.8.0",
81
+ "@testing-library/jest-dom": "^6.9.1",
82
82
  "@testing-library/user-event": "^14.6.1",
83
83
  "@types/jest": "^30.0.0",
84
84
  "@types/mithril": "^2.2.7",
@@ -86,16 +86,16 @@
86
86
  "concurrently": "^9.2.1",
87
87
  "express": "^5.1.0",
88
88
  "identity-obj-proxy": "^3.0.0",
89
- "jest": "^30.1.3",
90
- "jest-environment-jsdom": "^30.1.2",
89
+ "jest": "^30.2.0",
90
+ "jest-environment-jsdom": "^30.2.0",
91
91
  "js-yaml": "^4.1.0",
92
- "rimraf": "^6.0.1",
93
- "rollup": "^4.50.1",
92
+ "rimraf": "^6.1.0",
93
+ "rollup": "^4.52.5",
94
94
  "rollup-plugin-postcss": "^4.0.2",
95
- "sass": "^1.92.1",
96
- "ts-jest": "^29.4.1",
95
+ "sass": "^1.93.2",
96
+ "ts-jest": "^29.4.5",
97
97
  "tslib": "^2.8.1",
98
- "typedoc": "^0.28.12",
99
- "typescript": "^5.9.2"
98
+ "typedoc": "^0.28.14",
99
+ "typescript": "^5.9.3"
100
100
  }
101
101
  }
@@ -217,3 +217,26 @@ nav {
217
217
  height: variables.$navbar-height;
218
218
  }
219
219
  }
220
+
221
+ // Dark theme support
222
+ [data-theme="dark"] {
223
+ nav ul li.active {
224
+ background-color: rgba(38, 166, 154, 0.2);
225
+
226
+ a {
227
+ color: var(--mm-text-primary, rgba(255, 255, 255, 0.87));
228
+ }
229
+
230
+ i, .material-icons {
231
+ color: var(--mm-text-primary, rgba(255, 255, 255, 0.87));
232
+ }
233
+ }
234
+ }
235
+
236
+ // Make menu items non-selectable by default
237
+ nav ul li {
238
+ user-select: none;
239
+ -webkit-user-select: none;
240
+ -moz-user-select: none;
241
+ -ms-user-select: none;
242
+ }
@@ -130,6 +130,8 @@ ul.sidenav.right-aligned li > a:not(.btn):not(.btn-large):not(.btn-flat):not(.bt
130
130
  backface-visibility: hidden;
131
131
  transform: translateX(-105%);
132
132
  transition: transform 0.3s ease, left 0.3s ease, right 0.3s ease;
133
+ display: flex;
134
+ flex-direction: column;
133
135
 
134
136
  @extend .z-depth-1 !optional;
135
137
 
@@ -193,14 +195,17 @@ ul.sidenav.right-aligned li > a:not(.btn):not(.btn-large):not(.btn-flat):not(.bt
193
195
  li > a:not(.btn):not(.btn-large):not(.btn-flat):not(.btn-floating) {
194
196
  &:hover { background-color: var(--mm-border-color, rgba(0,0,0,.05));}
195
197
 
196
- // color: var(--mm-text-primary, variables.$sidenav-font-color);
197
- color: var(--mm-nav-active-text, #fff);
198
+ color: var(--mm-text-primary, variables.$sidenav-font-color);
198
199
  display: block;
199
200
  font-size: variables.$sidenav-font-size;
200
201
  font-weight: 500;
201
202
  height: variables.$sidenav-item-height;
202
203
  line-height: variables.$sidenav-line-height;
203
204
  padding: 0 (variables.$sidenav-padding * 2);
205
+ user-select: none;
206
+ -webkit-user-select: none;
207
+ -moz-user-select: none;
208
+ -ms-user-select: none;
204
209
 
205
210
  & > i,
206
211
  & > [class^="mdi-"], li > a > [class*="mdi-"],
@@ -215,6 +220,17 @@ ul.sidenav.right-aligned li > a:not(.btn):not(.btn-large):not(.btn-flat):not(.bt
215
220
  }
216
221
  }
217
222
 
223
+ // Active menu item styling
224
+ li.active > a:not(.btn):not(.btn-large):not(.btn-flat):not(.btn-floating) {
225
+ color: var(--mm-nav-active-text, #fff);
226
+ background-color: var(--mm-primary-color, #26a69a);
227
+
228
+ & > i,
229
+ & > i.material-icons {
230
+ color: var(--mm-nav-active-text, #fff);
231
+ }
232
+ }
233
+
218
234
  // Stlye btn anchors
219
235
  li > .btn, li > .btn-large, li > .btn-flat, li > .btn-floating {
220
236
  margin: 10px (variables.$sidenav-padding * 2);
@@ -452,17 +468,25 @@ ul.sidenav.right-aligned li > a:not(.btn):not(.btn-large):not(.btn-flat):not(.bt
452
468
  .sidenav-subitem {
453
469
  list-style: none;
454
470
  transition: background-color 0.2s ease;
471
+ user-select: none;
472
+ -webkit-user-select: none;
473
+ -moz-user-select: none;
474
+ -ms-user-select: none;
455
475
 
456
476
  &:hover {
457
477
  background: var(--mm-border-color, rgba(0, 0, 0, 0.05));
458
478
  }
459
479
 
460
- // Don't show selected background - only the check/radio icon indicates selection
461
- // to avoid confusion with multiple active menu items
462
480
  &.selected {
481
+ background-color: var(--mm-primary-color-light, rgba(38, 166, 154, 0.15));
482
+
463
483
  svg {
464
484
  fill: var(--mm-primary-color, #26a69a);
465
485
  }
486
+
487
+ i.material-icons {
488
+ color: var(--mm-primary-color, #26a69a);
489
+ }
466
490
  }
467
491
 
468
492
  svg {
@@ -510,24 +534,24 @@ ul.sidenav.right-aligned li > a:not(.btn):not(.btn-large):not(.btn-flat):not(.bt
510
534
  }
511
535
 
512
536
  li.active {
513
- background-color: rgba(255, 255, 255, 0.05);
537
+ background-color: var(--mm-primary-color, #26a69a);
514
538
 
515
539
  & > a:not(.btn):not(.btn-large):not(.btn-flat):not(.btn-floating) {
516
- color: var(--mm-nav-active-text, #fff);
540
+ color: #000;
517
541
 
518
542
  & > i,
519
543
  & > i.material-icons {
520
- color: var(--mm-nav-active-text, #fff);
544
+ color: #000;
521
545
  }
522
546
  }
523
547
  }
524
548
 
525
549
  .collapsible-body > ul:not(.collapsible) > li.active a {
526
- color: var(--mm-nav-active-text, #fff);
550
+ color: #000;
527
551
 
528
552
  i,
529
553
  i.material-icons {
530
- color: var(--mm-nav-active-text, #fff);
554
+ color: #000;
531
555
  }
532
556
  }
533
557
 
@@ -552,9 +576,27 @@ ul.sidenav.right-aligned li > a:not(.btn):not(.btn-large):not(.btn-flat):not(.bt
552
576
  i.material-icons {
553
577
  color: var(--mm-text-secondary, rgba(255, 255, 255, 0.6));
554
578
  }
579
+
580
+ &.selected {
581
+ background-color: rgba(38, 166, 154, 0.2);
582
+ color: var(--mm-text-primary, rgba(255, 255, 255, 0.87));
583
+
584
+ svg {
585
+ fill: var(--mm-primary-color, #26a69a);
586
+ }
587
+
588
+ i.material-icons {
589
+ color: var(--mm-primary-color, #26a69a);
590
+ }
591
+ }
555
592
  }
556
593
  }
557
594
 
595
+ // Footer item positioning
596
+ .sidenav-footer-item {
597
+ margin-top: auto;
598
+ }
599
+
558
600
  // Animation for smooth width transitions
559
601
  .sidenav {
560
602
  transition: transform 0.3s ease, width 0.3s ease;
@@ -123,7 +123,7 @@ body {
123
123
  // Navigation colors
124
124
  --mm-nav-background: #1e1e1e;
125
125
  --mm-nav-text: #ffffff;
126
- --mm-nav-active-text: #000000;
126
+ --mm-nav-active-text: #ffffff;
127
127
 
128
128
  // Modal and overlay colors
129
129
  --mm-modal-background: #2d2d2d;