mithril-materialized 3.5.3 → 3.5.4

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,66 @@
7814
7814
  };
7815
7815
  };
7816
7816
 
7817
+ // List of MaterialIcon SVG icons that are available
7818
+ const materialIconSvgNames = [
7819
+ 'caret', 'close', 'chevron', 'chevron_left', 'chevron_right', 'menu',
7820
+ 'expand', 'collapse', 'check', 'radio_checked', 'radio_unchecked',
7821
+ 'light_mode', 'dark_mode'
7822
+ ];
7823
+ /**
7824
+ * Helper function to render icons based on IconDefinition type
7825
+ */
7826
+ const renderIcon = (icon, style) => {
7827
+ if (!icon)
7828
+ return null;
7829
+ if (typeof icon === 'string') {
7830
+ // Check if this is a MaterialIcon SVG name
7831
+ if (materialIconSvgNames.includes(icon)) {
7832
+ return m(MaterialIcon, { name: icon, style });
7833
+ }
7834
+ // Fall back to Material Icons font for other icon names
7835
+ return m('i.material-icons', { style }, icon);
7836
+ }
7837
+ if (icon.type === 'svg') {
7838
+ // Inline SVG
7839
+ return m.trust(icon.content);
7840
+ }
7841
+ if (icon.type === 'image') {
7842
+ // Image URL
7843
+ return m('img', {
7844
+ src: icon.content,
7845
+ style: Object.assign(Object.assign({}, style), { width: '24px', height: '24px', objectFit: 'contain' }),
7846
+ });
7847
+ }
7848
+ return null;
7849
+ };
7850
+ /**
7851
+ * Helper function to render a single sidenav item (for header/footer items)
7852
+ */
7853
+ const renderSidenavItem = (item, isExpanded, position) => {
7854
+ const { text, icon, onclick, href, className = '' } = item;
7855
+ const isRightAligned = position === 'right';
7856
+ const content = isRightAligned
7857
+ ? [
7858
+ isExpanded && m('span.sidenav-item-text', { style: { 'flex': '1', 'text-align': 'left', 'margin-right': '8px' } }, text),
7859
+ renderIcon(icon, { 'min-width': '24px', 'width': '24px' }),
7860
+ ]
7861
+ : [
7862
+ renderIcon(icon, { 'min-width': '24px', 'width': '24px' }),
7863
+ isExpanded && m('span.sidenav-item-text', { style: { 'margin-left': '8px', 'flex': '1' } }, text),
7864
+ ];
7865
+ const linkStyle = {
7866
+ display: 'flex',
7867
+ 'align-items': 'center',
7868
+ padding: isExpanded ? '12px 16px' : '12px 18px',
7869
+ 'justify-content': isExpanded ? (isRightAligned ? 'flex-end' : 'flex-start') : 'center',
7870
+ };
7871
+ return m('li', { class: className }, m('a', {
7872
+ href: href || '#!',
7873
+ onclick: onclick,
7874
+ style: linkStyle,
7875
+ }, content));
7876
+ };
7817
7877
  /**
7818
7878
  * Sidenav Component
7819
7879
  * A responsive navigation drawer that slides in from the side
@@ -7936,6 +7996,8 @@
7936
7996
  name: 'menu',
7937
7997
  style: { width: '24px', height: '24px' },
7938
7998
  })),
7999
+ // Header item (if provided, appears before expand/collapse toggle)
8000
+ attrs.header && renderSidenavItem(attrs.header, isExpanded, position),
7939
8001
  // Expand/collapse toggle button (if expandable, right below hamburger)
7940
8002
  expandable &&
7941
8003
  m('li.sidenav-expand-toggle', {
@@ -7964,6 +8026,8 @@
7964
8026
  return child;
7965
8027
  })
7966
8028
  : children,
8029
+ // Footer item (if provided, appears at the bottom)
8030
+ attrs.footer && renderSidenavItem(attrs.footer, isExpanded, position),
7967
8031
  ]),
7968
8032
  ];
7969
8033
  },
@@ -7982,43 +8046,42 @@
7982
8046
  }
7983
8047
  };
7984
8048
  const isRightAligned = position === 'right';
8049
+ // Render indicator icon for checkbox/radio modes
8050
+ const indicatorIcon = mode !== 'none'
8051
+ ? m(MaterialIcon, {
8052
+ name: mode === 'checkbox' ? (selected ? 'check' : 'close') : selected ? 'radio_checked' : 'radio_unchecked',
8053
+ style: {
8054
+ width: '18px',
8055
+ height: '18px',
8056
+ opacity: mode === 'checkbox' && !selected ? '0.3' : '1',
8057
+ },
8058
+ })
8059
+ : null;
7985
8060
  const submenuContent = isRightAligned
7986
8061
  ? [
7987
8062
  // Right-aligned: text on left, icons on right
7988
8063
  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
- }),
8064
+ icon && isExpanded && renderIcon(icon, { 'font-size': '18px' }),
8065
+ indicatorIcon,
7998
8066
  ]
7999
8067
  : [
8000
8068
  // 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),
8069
+ indicatorIcon,
8070
+ icon && isExpanded && renderIcon(icon, { 'font-size': '18px', 'margin-left': indicatorIcon ? '8px' : '0' }),
8071
+ isExpanded && m('span', { style: { 'margin-left': icon || indicatorIcon ? '8px' : '0' } }, text),
8011
8072
  ];
8012
8073
  return m('li.sidenav-subitem', {
8013
8074
  class: selected ? 'selected' : '',
8014
8075
  style: {
8015
- padding: isExpanded ? '8px 16px 8px 48px' : '8px 16px',
8076
+ padding: isExpanded ? '0 16px 0 48px' : '0 16px',
8016
8077
  cursor: 'pointer',
8017
8078
  display: 'flex',
8018
8079
  'align-items': 'center',
8019
8080
  gap: '8px',
8020
8081
  'font-size': '0.9em',
8021
8082
  'justify-content': isRightAligned ? 'space-between' : 'flex-start',
8083
+ height: '48px',
8084
+ 'min-height': '48px',
8022
8085
  },
8023
8086
  onclick: handleClick,
8024
8087
  }, submenuContent);
@@ -8050,8 +8113,8 @@
8050
8113
  .filter(Boolean)
8051
8114
  .join(' ') || undefined;
8052
8115
  const handleMainClick = (e) => {
8053
- e.preventDefault();
8054
8116
  if (hasSubmenu) {
8117
+ e.preventDefault();
8055
8118
  isSubmenuOpen = active ? !isSubmenuOpen : true;
8056
8119
  }
8057
8120
  if (onclick && !disabled) {
@@ -8068,11 +8131,11 @@
8068
8131
  ? [
8069
8132
  // Right-aligned: text on left, icon on right
8070
8133
  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 || ''),
8134
+ renderIcon(icon, { 'min-width': '24px', 'width': '24px' }),
8072
8135
  ]
8073
8136
  : [
8074
8137
  // Left-aligned: icon on left, text on right
8075
- m('i.material-icons', { style: { 'min-width': '24px', 'width': '24px' } }, icon || ''),
8138
+ renderIcon(icon, { 'min-width': '24px', 'width': '24px' }),
8076
8139
  isExpanded && m('span.sidenav-item-text', { style: { 'margin-left': '8px', 'flex': '1' } }, text || children),
8077
8140
  ];
8078
8141
  const linkStyle = {
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.4",
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
+ }
@@ -193,14 +193,17 @@ ul.sidenav.right-aligned li > a:not(.btn):not(.btn-large):not(.btn-flat):not(.bt
193
193
  li > a:not(.btn):not(.btn-large):not(.btn-flat):not(.btn-floating) {
194
194
  &:hover { background-color: var(--mm-border-color, rgba(0,0,0,.05));}
195
195
 
196
- // color: var(--mm-text-primary, variables.$sidenav-font-color);
197
- color: var(--mm-nav-active-text, #fff);
196
+ color: var(--mm-text-primary, variables.$sidenav-font-color);
198
197
  display: block;
199
198
  font-size: variables.$sidenav-font-size;
200
199
  font-weight: 500;
201
200
  height: variables.$sidenav-item-height;
202
201
  line-height: variables.$sidenav-line-height;
203
202
  padding: 0 (variables.$sidenav-padding * 2);
203
+ user-select: none;
204
+ -webkit-user-select: none;
205
+ -moz-user-select: none;
206
+ -ms-user-select: none;
204
207
 
205
208
  & > i,
206
209
  & > [class^="mdi-"], li > a > [class*="mdi-"],
@@ -215,6 +218,17 @@ ul.sidenav.right-aligned li > a:not(.btn):not(.btn-large):not(.btn-flat):not(.bt
215
218
  }
216
219
  }
217
220
 
221
+ // Active menu item styling
222
+ li.active > a:not(.btn):not(.btn-large):not(.btn-flat):not(.btn-floating) {
223
+ color: var(--mm-nav-active-text, #fff);
224
+ background-color: var(--mm-primary-color, #26a69a);
225
+
226
+ & > i,
227
+ & > i.material-icons {
228
+ color: var(--mm-nav-active-text, #fff);
229
+ }
230
+ }
231
+
218
232
  // Stlye btn anchors
219
233
  li > .btn, li > .btn-large, li > .btn-flat, li > .btn-floating {
220
234
  margin: 10px (variables.$sidenav-padding * 2);
@@ -452,17 +466,25 @@ ul.sidenav.right-aligned li > a:not(.btn):not(.btn-large):not(.btn-flat):not(.bt
452
466
  .sidenav-subitem {
453
467
  list-style: none;
454
468
  transition: background-color 0.2s ease;
469
+ user-select: none;
470
+ -webkit-user-select: none;
471
+ -moz-user-select: none;
472
+ -ms-user-select: none;
455
473
 
456
474
  &:hover {
457
475
  background: var(--mm-border-color, rgba(0, 0, 0, 0.05));
458
476
  }
459
477
 
460
- // Don't show selected background - only the check/radio icon indicates selection
461
- // to avoid confusion with multiple active menu items
462
478
  &.selected {
479
+ background-color: var(--mm-primary-color-light, rgba(38, 166, 154, 0.15));
480
+
463
481
  svg {
464
482
  fill: var(--mm-primary-color, #26a69a);
465
483
  }
484
+
485
+ i.material-icons {
486
+ color: var(--mm-primary-color, #26a69a);
487
+ }
466
488
  }
467
489
 
468
490
  svg {
@@ -510,24 +532,24 @@ ul.sidenav.right-aligned li > a:not(.btn):not(.btn-large):not(.btn-flat):not(.bt
510
532
  }
511
533
 
512
534
  li.active {
513
- background-color: rgba(255, 255, 255, 0.05);
535
+ background-color: var(--mm-primary-color, #26a69a);
514
536
 
515
537
  & > a:not(.btn):not(.btn-large):not(.btn-flat):not(.btn-floating) {
516
- color: var(--mm-nav-active-text, #fff);
538
+ color: #000;
517
539
 
518
540
  & > i,
519
541
  & > i.material-icons {
520
- color: var(--mm-nav-active-text, #fff);
542
+ color: #000;
521
543
  }
522
544
  }
523
545
  }
524
546
 
525
547
  .collapsible-body > ul:not(.collapsible) > li.active a {
526
- color: var(--mm-nav-active-text, #fff);
548
+ color: #000;
527
549
 
528
550
  i,
529
551
  i.material-icons {
530
- color: var(--mm-nav-active-text, #fff);
552
+ color: #000;
531
553
  }
532
554
  }
533
555
 
@@ -552,6 +574,19 @@ ul.sidenav.right-aligned li > a:not(.btn):not(.btn-large):not(.btn-flat):not(.bt
552
574
  i.material-icons {
553
575
  color: var(--mm-text-secondary, rgba(255, 255, 255, 0.6));
554
576
  }
577
+
578
+ &.selected {
579
+ background-color: rgba(38, 166, 154, 0.2);
580
+ color: var(--mm-text-primary, rgba(255, 255, 255, 0.87));
581
+
582
+ svg {
583
+ fill: var(--mm-primary-color, #26a69a);
584
+ }
585
+
586
+ i.material-icons {
587
+ color: var(--mm-primary-color, #26a69a);
588
+ }
589
+ }
555
590
  }
556
591
  }
557
592
 
@@ -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;