mithril-materialized 3.4.4 → 3.5.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.esm.js CHANGED
@@ -264,6 +264,7 @@ const Autocomplete = () => {
264
264
  state.isOpen = false;
265
265
  state.selectedIndex = -1;
266
266
  }
267
+ m.redraw();
267
268
  };
268
269
  const getDropdownStyles = () => {
269
270
  if (!state.inputElement) {
@@ -962,6 +963,18 @@ const iconPaths = {
962
963
  'M16.59 8.59L12 13.17 7.41 8.59 6 10l6 6 6-6z', // chevron down
963
964
  'M0 0h24v24H0z', // background
964
965
  ],
966
+ chevron_left: [
967
+ 'M15.41 7.41L14 6l-6 6 6 6 1.41-1.41L10.83 12z', // chevron left
968
+ 'M0 0h24v24H0z', // background
969
+ ],
970
+ chevron_right: [
971
+ 'M10 6L8.59 7.41 13.17 12l-4.58 4.59L10 18l6-6z', // chevron right
972
+ 'M0 0h24v24H0z', // background
973
+ ],
974
+ menu: [
975
+ 'M3 18h18v-2H3v2zm0-5h18v-2H3v2zm0-7v2h18V6H3z', // hamburger menu
976
+ 'M0 0h24v24H0z', // background
977
+ ],
965
978
  expand: [
966
979
  'M19 13h-6v6h-2v-6H5v-2h6V5h2v6h6v2z', // plus
967
980
  'M0 0h24v24H0z', // background
@@ -970,6 +983,18 @@ const iconPaths = {
970
983
  'M19 13H5v-2h14v2z', // minus
971
984
  'M0 0h24v24H0z', // background
972
985
  ],
986
+ check: [
987
+ 'M9 16.17L4.83 12l-1.42 1.41L9 19 21 7l-1.41-1.41z', // checkmark
988
+ 'M0 0h24v24H0z', // background
989
+ ],
990
+ radio_checked: [
991
+ 'M12 7c-2.76 0-5 2.24-5 5s2.24 5 5 5 5-2.24 5-5-2.24-5-5-5zm0-5C6.48 2 2 6.48 2 12s4.48 10 10 10 10-4.48 10-10S17.52 2 12 2zm0 18c-4.42 0-8-3.58-8-8s3.58-8 8-8 8 3.58 8 8-3.58 8-8 8z', // radio button checked
992
+ 'M0 0h24v24H0z', // background
993
+ ],
994
+ radio_unchecked: [
995
+ 'M12 2C6.48 2 2 6.48 2 12s4.48 10 10 10 10-4.48 10-10S17.52 2 12 2zm0 18c-4.42 0-8-3.58-8-8s3.58-8 8-8 8 3.58 8 8-3.58 8-8 8z', // radio button unchecked
996
+ 'M0 0h24v24H0z', // background
997
+ ],
973
998
  light_mode: [
974
999
  'M12 7c-2.76 0-5 2.24-5 5s2.24 5 5 5 5-2.24 5-5-2.24-5-5-5M2 13h2c.55 0 1-.45 1-1s-.45-1-1-1H2c-.55 0-1 .45-1 1s.45 1 1 1m18 0h2c.55 0 1-.45 1-1s-.45-1-1-1h-2c-.55 0-1 .45-1 1s.45 1 1 1M11 2v2c0 .55.45 1 1 1s1-.45 1-1V2c0-.55-.45-1-1-1s-1 .45-1 1m0 18v2c0 .55.45 1 1 1s1-.45 1-1v-2c0-.55-.45-1-1-1s-1 .45-1 1M5.99 4.58a.996.996 0 0 0-1.41 0 .996.996 0 0 0 0 1.41l1.06 1.06c.39.39 1.03.39 1.41 0s.39-1.03 0-1.41zm12.4 12.4a.996.996 0 0 0-1.41 0 .996.996 0 0 0 0 1.41l1.06 1.06c.39.39 1.03.39 1.41 0a.996.996 0 0 0 0-1.41zm1.06-11a.996.996 0 0 0 0-1.41.996.996 0 0 0-1.41 0l-1.06 1.06c-.39.39-.39 1.03 0 1.41s1.03.39 1.41 0zM7.05 18.4a.996.996 0 0 0 0-1.41.996.996 0 0 0-1.41 0l-1.06 1.06c-.39.39-.39 1.03 0 1.41s1.03.39 1.41 0z',
975
1000
  'M0 0h24v24H0z', // background
@@ -7796,12 +7821,27 @@ const Sidenav = () => {
7796
7821
  document.body.style.overflow = isOpen && mode === 'overlay' ? 'hidden' : '';
7797
7822
  }
7798
7823
  };
7824
+ const toggleExpanded = (attrs) => {
7825
+ const newExpandedState = !(attrs.isExpanded !== false);
7826
+ if (attrs.onExpandChange) {
7827
+ attrs.onExpandChange(newExpandedState);
7828
+ }
7829
+ };
7830
+ const toggleHamburger = (attrs) => {
7831
+ const newOpenState = !state.isOpen;
7832
+ if (attrs.onToggle) {
7833
+ attrs.onToggle(newOpenState);
7834
+ }
7835
+ };
7799
7836
  return {
7800
7837
  oninit: ({ attrs }) => {
7801
7838
  state = {
7802
7839
  id: attrs.id || uniqueId(),
7803
7840
  isOpen: attrs.isOpen || false,
7804
7841
  isAnimating: false,
7842
+ isExpanded: attrs.isExpanded !== false,
7843
+ activeItemIndex: null,
7844
+ selectedSubmenuItems: new Map(),
7805
7845
  };
7806
7846
  // Set up keyboard listener
7807
7847
  if (typeof document !== 'undefined' && attrs.closeOnEscape !== false) {
@@ -7830,12 +7870,16 @@ const Sidenav = () => {
7830
7870
  }
7831
7871
  },
7832
7872
  view: ({ attrs, children }) => {
7833
- const { position = 'left', mode = 'overlay', width = 300, className = '', showBackdrop = true, animationDuration = 300, fixed = false, } = attrs;
7873
+ const { position = 'left', mode = 'overlay', width = 300, className = '', showBackdrop = true, animationDuration = 300, fixed = false, showHamburger = false, expandable = false, } = attrs;
7834
7874
  const isOpen = state.isOpen;
7875
+ const collapsedWidth = 60;
7876
+ const isExpanded = attrs.isExpanded !== false;
7877
+ const currentWidth = expandable && !isExpanded ? collapsedWidth : width;
7835
7878
  return [
7836
- // Backdrop (using existing materialize class)
7879
+ // Backdrop (using existing materialize class) - only for overlay mode
7837
7880
  showBackdrop &&
7838
7881
  mode === 'overlay' &&
7882
+ !fixed &&
7839
7883
  m('.sidenav-overlay', {
7840
7884
  style: {
7841
7885
  display: isOpen ? 'block' : 'none',
@@ -7846,49 +7890,206 @@ const Sidenav = () => {
7846
7890
  // Sidenav (using existing materialize structure)
7847
7891
  m('ul.sidenav', {
7848
7892
  id: state.id,
7849
- class: [position === 'right' ? 'right-aligned' : '', fixed ? 'sidenav-fixed' : '', className]
7893
+ class: [
7894
+ position === 'right' ? 'right-aligned' : '',
7895
+ fixed ? 'sidenav-fixed' : '',
7896
+ expandable && !isExpanded ? 'sidenav-collapsed' : '',
7897
+ className,
7898
+ ]
7850
7899
  .filter(Boolean)
7851
7900
  .join(' ') || undefined,
7852
7901
  style: {
7853
- width: `${width}px`,
7902
+ width: `${currentWidth}px`,
7854
7903
  transform: isOpen ? 'translateX(0)' : position === 'left' ? 'translateX(-105%)' : 'translateX(105%)',
7855
7904
  'transition-duration': `${animationDuration}ms`,
7905
+ 'transition-property': 'transform, width',
7856
7906
  },
7857
- }, children),
7907
+ }, [
7908
+ // Hamburger toggle button (inside sidenav, at the top)
7909
+ showHamburger &&
7910
+ m('li.sidenav-hamburger-item', {
7911
+ style: {
7912
+ display: 'flex',
7913
+ 'justify-content': position === 'right' ? 'flex-end' : 'flex-start',
7914
+ 'align-items': 'center',
7915
+ padding: '12px 16px',
7916
+ cursor: 'pointer',
7917
+ 'border-bottom': '1px solid rgba(0,0,0,0.1)',
7918
+ },
7919
+ onclick: () => toggleHamburger(attrs),
7920
+ }, m(MaterialIcon, {
7921
+ name: 'menu',
7922
+ style: { width: '24px', height: '24px' },
7923
+ })),
7924
+ // Expand/collapse toggle button (if expandable, right below hamburger)
7925
+ expandable &&
7926
+ m('li.sidenav-expand-toggle', {
7927
+ style: {
7928
+ display: 'flex',
7929
+ 'justify-content': position === 'right' ? 'flex-end' : 'flex-start',
7930
+ 'align-items': 'center',
7931
+ padding: '12px 16px',
7932
+ cursor: 'pointer',
7933
+ 'border-bottom': '1px solid rgba(0,0,0,0.1)',
7934
+ },
7935
+ onclick: () => toggleExpanded(attrs),
7936
+ }, m(MaterialIcon, {
7937
+ name: position === 'right'
7938
+ ? (isExpanded ? 'chevron_right' : 'chevron_left')
7939
+ : (isExpanded ? 'chevron_left' : 'chevron_right'),
7940
+ style: { width: '24px', height: '24px' },
7941
+ })),
7942
+ // Children (menu items) - inject internal props
7943
+ Array.isArray(children)
7944
+ ? children.map((child) => {
7945
+ if (child && typeof child === 'object' && 'tag' in child) {
7946
+ // Clone the vnode and add internal props
7947
+ return Object.assign(Object.assign({}, child), { attrs: Object.assign(Object.assign({}, child.attrs), { _isExpanded: isExpanded, _position: position }) });
7948
+ }
7949
+ return child;
7950
+ })
7951
+ : children,
7952
+ ]),
7858
7953
  ];
7859
7954
  },
7860
7955
  };
7861
7956
  };
7957
+ /**
7958
+ * Sidenav Submenu Item Component
7959
+ */
7960
+ const NavbarSubItem = () => {
7961
+ return {
7962
+ view: ({ attrs }) => {
7963
+ const { text, icon, selected = false, value, onSelect, mode, isExpanded, position = 'left' } = attrs;
7964
+ const handleClick = () => {
7965
+ if (onSelect) {
7966
+ onSelect(value !== undefined ? value : text, !selected);
7967
+ }
7968
+ };
7969
+ const isRightAligned = position === 'right';
7970
+ const submenuContent = isRightAligned
7971
+ ? [
7972
+ // Right-aligned: text on left, icons on right
7973
+ isExpanded && m('span', { style: { 'flex': '1', 'text-align': 'left' } }, text),
7974
+ icon && isExpanded && m('i.material-icons', { style: { 'font-size': '18px' } }, icon),
7975
+ m(MaterialIcon, {
7976
+ name: mode === 'checkbox' ? (selected ? 'check' : 'close') : selected ? 'radio_checked' : 'radio_unchecked',
7977
+ style: {
7978
+ width: '18px',
7979
+ height: '18px',
7980
+ opacity: mode === 'checkbox' && !selected ? '0.3' : '1',
7981
+ },
7982
+ }),
7983
+ ]
7984
+ : [
7985
+ // Left-aligned: indicator on left, text and icon on right
7986
+ m(MaterialIcon, {
7987
+ name: mode === 'checkbox' ? (selected ? 'check' : 'close') : selected ? 'radio_checked' : 'radio_unchecked',
7988
+ style: {
7989
+ width: '18px',
7990
+ height: '18px',
7991
+ opacity: mode === 'checkbox' && !selected ? '0.3' : '1',
7992
+ },
7993
+ }),
7994
+ icon && isExpanded && m('i.material-icons', { style: { 'font-size': '18px', 'margin-left': '8px' } }, icon),
7995
+ isExpanded && m('span', { style: { 'margin-left': icon ? '8px' : '8px' } }, text),
7996
+ ];
7997
+ return m('li.sidenav-subitem', {
7998
+ class: selected ? 'selected' : '',
7999
+ style: {
8000
+ padding: isExpanded ? '8px 16px 8px 48px' : '8px 16px',
8001
+ cursor: 'pointer',
8002
+ display: 'flex',
8003
+ 'align-items': 'center',
8004
+ gap: '8px',
8005
+ 'font-size': '0.9em',
8006
+ 'justify-content': isRightAligned ? 'space-between' : 'flex-start',
8007
+ },
8008
+ onclick: handleClick,
8009
+ }, submenuContent);
8010
+ },
8011
+ };
8012
+ };
7862
8013
  /**
7863
8014
  * Sidenav Item Component
7864
8015
  * Individual items for the sidenav menu
7865
8016
  */
7866
8017
  const SidenavItem = () => {
8018
+ let isSubmenuOpen = false;
7867
8019
  return {
7868
8020
  view: ({ attrs, children }) => {
7869
- const { text, icon, active = false, disabled = false, onclick, href, className = '', divider = false, subheader = false, } = attrs;
8021
+ const { text, icon, active = false, disabled = false, onclick, href, className = '', divider = false, subheader = false, submenu = [], submenuMode = 'checkbox', } = attrs;
7870
8022
  if (divider) {
7871
8023
  return m('li.divider');
7872
8024
  }
7873
8025
  if (subheader) {
7874
8026
  return m('li.subheader', text || children);
7875
8027
  }
7876
- const itemClasses = [active ? 'active' : '', disabled ? 'disabled' : '', className].filter(Boolean).join(' ') || undefined;
7877
- const content = [icon && m('i.material-icons', icon), text || children];
7878
- if (href && !disabled) {
7879
- return m('li', { class: itemClasses }, [
8028
+ const hasSubmenu = submenu && submenu.length > 0;
8029
+ const itemClasses = [
8030
+ active ? 'active' : '',
8031
+ disabled ? 'disabled' : '',
8032
+ hasSubmenu ? 'has-submenu' : '',
8033
+ className,
8034
+ ]
8035
+ .filter(Boolean)
8036
+ .join(' ') || undefined;
8037
+ const handleMainClick = (e) => {
8038
+ e.preventDefault();
8039
+ if (hasSubmenu) {
8040
+ isSubmenuOpen = active ? !isSubmenuOpen : true;
8041
+ }
8042
+ if (onclick && !disabled) {
8043
+ onclick(e);
8044
+ }
8045
+ };
8046
+ // Get internal props passed from parent Sidenav
8047
+ const isExpanded = attrs._isExpanded !== false;
8048
+ const position = attrs._position || 'left';
8049
+ const isRightAligned = position === 'right';
8050
+ // In expanded mode, icons are at the outside edge
8051
+ // In collapsed mode, icons are centered
8052
+ const content = isRightAligned
8053
+ ? [
8054
+ // Right-aligned: text on left, icon on right
8055
+ isExpanded && m('span.sidenav-item-text', { style: { 'flex': '1', 'text-align': 'left', 'margin-right': '8px' } }, text || children),
8056
+ m('i.material-icons', { style: { 'min-width': '24px', 'width': '24px' } }, icon || ''),
8057
+ ]
8058
+ : [
8059
+ // Left-aligned: icon on left, text on right
8060
+ m('i.material-icons', { style: { 'min-width': '24px', 'width': '24px' } }, icon || ''),
8061
+ isExpanded && m('span.sidenav-item-text', { style: { 'margin-left': '8px', 'flex': '1' } }, text || children),
8062
+ ];
8063
+ const linkStyle = {
8064
+ display: 'flex',
8065
+ 'align-items': 'center',
8066
+ padding: isExpanded ? '12px 16px' : '12px 18px',
8067
+ 'justify-content': isExpanded ? (isRightAligned ? 'flex-end' : 'flex-start') : 'center',
8068
+ };
8069
+ const mainItem = href && !disabled
8070
+ ? m('li', { class: itemClasses }, [
7880
8071
  m('a', {
7881
8072
  href,
7882
- onclick: disabled ? undefined : onclick,
8073
+ onclick: handleMainClick,
8074
+ style: linkStyle,
8075
+ }, content),
8076
+ ])
8077
+ : m('li', { class: itemClasses }, [
8078
+ m('a', {
8079
+ onclick: handleMainClick,
8080
+ href: '#!',
8081
+ style: linkStyle,
7883
8082
  }, content),
7884
8083
  ]);
8084
+ // Return main item with submenu if applicable
8085
+ if (hasSubmenu && active && isSubmenuOpen) {
8086
+ return [
8087
+ mainItem,
8088
+ submenu.map((subItem) => m(NavbarSubItem, Object.assign(Object.assign({}, subItem), { mode: submenuMode, isExpanded,
8089
+ position }))),
8090
+ ];
7885
8091
  }
7886
- return m('li', { class: itemClasses }, [
7887
- m('a', {
7888
- onclick: disabled ? undefined : onclick,
7889
- href: '#!',
7890
- }, content),
7891
- ]);
8092
+ return mainItem;
7892
8093
  },
7893
8094
  };
7894
8095
  };
package/dist/index.js CHANGED
@@ -266,6 +266,7 @@ const Autocomplete = () => {
266
266
  state.isOpen = false;
267
267
  state.selectedIndex = -1;
268
268
  }
269
+ m.redraw();
269
270
  };
270
271
  const getDropdownStyles = () => {
271
272
  if (!state.inputElement) {
@@ -964,6 +965,18 @@ const iconPaths = {
964
965
  'M16.59 8.59L12 13.17 7.41 8.59 6 10l6 6 6-6z', // chevron down
965
966
  'M0 0h24v24H0z', // background
966
967
  ],
968
+ chevron_left: [
969
+ 'M15.41 7.41L14 6l-6 6 6 6 1.41-1.41L10.83 12z', // chevron left
970
+ 'M0 0h24v24H0z', // background
971
+ ],
972
+ chevron_right: [
973
+ 'M10 6L8.59 7.41 13.17 12l-4.58 4.59L10 18l6-6z', // chevron right
974
+ 'M0 0h24v24H0z', // background
975
+ ],
976
+ menu: [
977
+ 'M3 18h18v-2H3v2zm0-5h18v-2H3v2zm0-7v2h18V6H3z', // hamburger menu
978
+ 'M0 0h24v24H0z', // background
979
+ ],
967
980
  expand: [
968
981
  'M19 13h-6v6h-2v-6H5v-2h6V5h2v6h6v2z', // plus
969
982
  'M0 0h24v24H0z', // background
@@ -972,6 +985,18 @@ const iconPaths = {
972
985
  'M19 13H5v-2h14v2z', // minus
973
986
  'M0 0h24v24H0z', // background
974
987
  ],
988
+ check: [
989
+ 'M9 16.17L4.83 12l-1.42 1.41L9 19 21 7l-1.41-1.41z', // checkmark
990
+ 'M0 0h24v24H0z', // background
991
+ ],
992
+ radio_checked: [
993
+ 'M12 7c-2.76 0-5 2.24-5 5s2.24 5 5 5 5-2.24 5-5-2.24-5-5-5zm0-5C6.48 2 2 6.48 2 12s4.48 10 10 10 10-4.48 10-10S17.52 2 12 2zm0 18c-4.42 0-8-3.58-8-8s3.58-8 8-8 8 3.58 8 8-3.58 8-8 8z', // radio button checked
994
+ 'M0 0h24v24H0z', // background
995
+ ],
996
+ radio_unchecked: [
997
+ 'M12 2C6.48 2 2 6.48 2 12s4.48 10 10 10 10-4.48 10-10S17.52 2 12 2zm0 18c-4.42 0-8-3.58-8-8s3.58-8 8-8 8 3.58 8 8-3.58 8-8 8z', // radio button unchecked
998
+ 'M0 0h24v24H0z', // background
999
+ ],
975
1000
  light_mode: [
976
1001
  'M12 7c-2.76 0-5 2.24-5 5s2.24 5 5 5 5-2.24 5-5-2.24-5-5-5M2 13h2c.55 0 1-.45 1-1s-.45-1-1-1H2c-.55 0-1 .45-1 1s.45 1 1 1m18 0h2c.55 0 1-.45 1-1s-.45-1-1-1h-2c-.55 0-1 .45-1 1s.45 1 1 1M11 2v2c0 .55.45 1 1 1s1-.45 1-1V2c0-.55-.45-1-1-1s-1 .45-1 1m0 18v2c0 .55.45 1 1 1s1-.45 1-1v-2c0-.55-.45-1-1-1s-1 .45-1 1M5.99 4.58a.996.996 0 0 0-1.41 0 .996.996 0 0 0 0 1.41l1.06 1.06c.39.39 1.03.39 1.41 0s.39-1.03 0-1.41zm12.4 12.4a.996.996 0 0 0-1.41 0 .996.996 0 0 0 0 1.41l1.06 1.06c.39.39 1.03.39 1.41 0a.996.996 0 0 0 0-1.41zm1.06-11a.996.996 0 0 0 0-1.41.996.996 0 0 0-1.41 0l-1.06 1.06c-.39.39-.39 1.03 0 1.41s1.03.39 1.41 0zM7.05 18.4a.996.996 0 0 0 0-1.41.996.996 0 0 0-1.41 0l-1.06 1.06c-.39.39-.39 1.03 0 1.41s1.03.39 1.41 0z',
977
1002
  'M0 0h24v24H0z', // background
@@ -7798,12 +7823,27 @@ const Sidenav = () => {
7798
7823
  document.body.style.overflow = isOpen && mode === 'overlay' ? 'hidden' : '';
7799
7824
  }
7800
7825
  };
7826
+ const toggleExpanded = (attrs) => {
7827
+ const newExpandedState = !(attrs.isExpanded !== false);
7828
+ if (attrs.onExpandChange) {
7829
+ attrs.onExpandChange(newExpandedState);
7830
+ }
7831
+ };
7832
+ const toggleHamburger = (attrs) => {
7833
+ const newOpenState = !state.isOpen;
7834
+ if (attrs.onToggle) {
7835
+ attrs.onToggle(newOpenState);
7836
+ }
7837
+ };
7801
7838
  return {
7802
7839
  oninit: ({ attrs }) => {
7803
7840
  state = {
7804
7841
  id: attrs.id || uniqueId(),
7805
7842
  isOpen: attrs.isOpen || false,
7806
7843
  isAnimating: false,
7844
+ isExpanded: attrs.isExpanded !== false,
7845
+ activeItemIndex: null,
7846
+ selectedSubmenuItems: new Map(),
7807
7847
  };
7808
7848
  // Set up keyboard listener
7809
7849
  if (typeof document !== 'undefined' && attrs.closeOnEscape !== false) {
@@ -7832,12 +7872,16 @@ const Sidenav = () => {
7832
7872
  }
7833
7873
  },
7834
7874
  view: ({ attrs, children }) => {
7835
- const { position = 'left', mode = 'overlay', width = 300, className = '', showBackdrop = true, animationDuration = 300, fixed = false, } = attrs;
7875
+ const { position = 'left', mode = 'overlay', width = 300, className = '', showBackdrop = true, animationDuration = 300, fixed = false, showHamburger = false, expandable = false, } = attrs;
7836
7876
  const isOpen = state.isOpen;
7877
+ const collapsedWidth = 60;
7878
+ const isExpanded = attrs.isExpanded !== false;
7879
+ const currentWidth = expandable && !isExpanded ? collapsedWidth : width;
7837
7880
  return [
7838
- // Backdrop (using existing materialize class)
7881
+ // Backdrop (using existing materialize class) - only for overlay mode
7839
7882
  showBackdrop &&
7840
7883
  mode === 'overlay' &&
7884
+ !fixed &&
7841
7885
  m('.sidenav-overlay', {
7842
7886
  style: {
7843
7887
  display: isOpen ? 'block' : 'none',
@@ -7848,49 +7892,206 @@ const Sidenav = () => {
7848
7892
  // Sidenav (using existing materialize structure)
7849
7893
  m('ul.sidenav', {
7850
7894
  id: state.id,
7851
- class: [position === 'right' ? 'right-aligned' : '', fixed ? 'sidenav-fixed' : '', className]
7895
+ class: [
7896
+ position === 'right' ? 'right-aligned' : '',
7897
+ fixed ? 'sidenav-fixed' : '',
7898
+ expandable && !isExpanded ? 'sidenav-collapsed' : '',
7899
+ className,
7900
+ ]
7852
7901
  .filter(Boolean)
7853
7902
  .join(' ') || undefined,
7854
7903
  style: {
7855
- width: `${width}px`,
7904
+ width: `${currentWidth}px`,
7856
7905
  transform: isOpen ? 'translateX(0)' : position === 'left' ? 'translateX(-105%)' : 'translateX(105%)',
7857
7906
  'transition-duration': `${animationDuration}ms`,
7907
+ 'transition-property': 'transform, width',
7858
7908
  },
7859
- }, children),
7909
+ }, [
7910
+ // Hamburger toggle button (inside sidenav, at the top)
7911
+ showHamburger &&
7912
+ m('li.sidenav-hamburger-item', {
7913
+ style: {
7914
+ display: 'flex',
7915
+ 'justify-content': position === 'right' ? 'flex-end' : 'flex-start',
7916
+ 'align-items': 'center',
7917
+ padding: '12px 16px',
7918
+ cursor: 'pointer',
7919
+ 'border-bottom': '1px solid rgba(0,0,0,0.1)',
7920
+ },
7921
+ onclick: () => toggleHamburger(attrs),
7922
+ }, m(MaterialIcon, {
7923
+ name: 'menu',
7924
+ style: { width: '24px', height: '24px' },
7925
+ })),
7926
+ // Expand/collapse toggle button (if expandable, right below hamburger)
7927
+ expandable &&
7928
+ m('li.sidenav-expand-toggle', {
7929
+ style: {
7930
+ display: 'flex',
7931
+ 'justify-content': position === 'right' ? 'flex-end' : 'flex-start',
7932
+ 'align-items': 'center',
7933
+ padding: '12px 16px',
7934
+ cursor: 'pointer',
7935
+ 'border-bottom': '1px solid rgba(0,0,0,0.1)',
7936
+ },
7937
+ onclick: () => toggleExpanded(attrs),
7938
+ }, m(MaterialIcon, {
7939
+ name: position === 'right'
7940
+ ? (isExpanded ? 'chevron_right' : 'chevron_left')
7941
+ : (isExpanded ? 'chevron_left' : 'chevron_right'),
7942
+ style: { width: '24px', height: '24px' },
7943
+ })),
7944
+ // Children (menu items) - inject internal props
7945
+ Array.isArray(children)
7946
+ ? children.map((child) => {
7947
+ if (child && typeof child === 'object' && 'tag' in child) {
7948
+ // Clone the vnode and add internal props
7949
+ return Object.assign(Object.assign({}, child), { attrs: Object.assign(Object.assign({}, child.attrs), { _isExpanded: isExpanded, _position: position }) });
7950
+ }
7951
+ return child;
7952
+ })
7953
+ : children,
7954
+ ]),
7860
7955
  ];
7861
7956
  },
7862
7957
  };
7863
7958
  };
7959
+ /**
7960
+ * Sidenav Submenu Item Component
7961
+ */
7962
+ const NavbarSubItem = () => {
7963
+ return {
7964
+ view: ({ attrs }) => {
7965
+ const { text, icon, selected = false, value, onSelect, mode, isExpanded, position = 'left' } = attrs;
7966
+ const handleClick = () => {
7967
+ if (onSelect) {
7968
+ onSelect(value !== undefined ? value : text, !selected);
7969
+ }
7970
+ };
7971
+ const isRightAligned = position === 'right';
7972
+ const submenuContent = isRightAligned
7973
+ ? [
7974
+ // Right-aligned: text on left, icons on right
7975
+ isExpanded && m('span', { style: { 'flex': '1', 'text-align': 'left' } }, text),
7976
+ icon && isExpanded && m('i.material-icons', { style: { 'font-size': '18px' } }, icon),
7977
+ m(MaterialIcon, {
7978
+ name: mode === 'checkbox' ? (selected ? 'check' : 'close') : selected ? 'radio_checked' : 'radio_unchecked',
7979
+ style: {
7980
+ width: '18px',
7981
+ height: '18px',
7982
+ opacity: mode === 'checkbox' && !selected ? '0.3' : '1',
7983
+ },
7984
+ }),
7985
+ ]
7986
+ : [
7987
+ // Left-aligned: indicator on left, text and icon on right
7988
+ m(MaterialIcon, {
7989
+ name: mode === 'checkbox' ? (selected ? 'check' : 'close') : selected ? 'radio_checked' : 'radio_unchecked',
7990
+ style: {
7991
+ width: '18px',
7992
+ height: '18px',
7993
+ opacity: mode === 'checkbox' && !selected ? '0.3' : '1',
7994
+ },
7995
+ }),
7996
+ icon && isExpanded && m('i.material-icons', { style: { 'font-size': '18px', 'margin-left': '8px' } }, icon),
7997
+ isExpanded && m('span', { style: { 'margin-left': icon ? '8px' : '8px' } }, text),
7998
+ ];
7999
+ return m('li.sidenav-subitem', {
8000
+ class: selected ? 'selected' : '',
8001
+ style: {
8002
+ padding: isExpanded ? '8px 16px 8px 48px' : '8px 16px',
8003
+ cursor: 'pointer',
8004
+ display: 'flex',
8005
+ 'align-items': 'center',
8006
+ gap: '8px',
8007
+ 'font-size': '0.9em',
8008
+ 'justify-content': isRightAligned ? 'space-between' : 'flex-start',
8009
+ },
8010
+ onclick: handleClick,
8011
+ }, submenuContent);
8012
+ },
8013
+ };
8014
+ };
7864
8015
  /**
7865
8016
  * Sidenav Item Component
7866
8017
  * Individual items for the sidenav menu
7867
8018
  */
7868
8019
  const SidenavItem = () => {
8020
+ let isSubmenuOpen = false;
7869
8021
  return {
7870
8022
  view: ({ attrs, children }) => {
7871
- const { text, icon, active = false, disabled = false, onclick, href, className = '', divider = false, subheader = false, } = attrs;
8023
+ const { text, icon, active = false, disabled = false, onclick, href, className = '', divider = false, subheader = false, submenu = [], submenuMode = 'checkbox', } = attrs;
7872
8024
  if (divider) {
7873
8025
  return m('li.divider');
7874
8026
  }
7875
8027
  if (subheader) {
7876
8028
  return m('li.subheader', text || children);
7877
8029
  }
7878
- const itemClasses = [active ? 'active' : '', disabled ? 'disabled' : '', className].filter(Boolean).join(' ') || undefined;
7879
- const content = [icon && m('i.material-icons', icon), text || children];
7880
- if (href && !disabled) {
7881
- return m('li', { class: itemClasses }, [
8030
+ const hasSubmenu = submenu && submenu.length > 0;
8031
+ const itemClasses = [
8032
+ active ? 'active' : '',
8033
+ disabled ? 'disabled' : '',
8034
+ hasSubmenu ? 'has-submenu' : '',
8035
+ className,
8036
+ ]
8037
+ .filter(Boolean)
8038
+ .join(' ') || undefined;
8039
+ const handleMainClick = (e) => {
8040
+ e.preventDefault();
8041
+ if (hasSubmenu) {
8042
+ isSubmenuOpen = active ? !isSubmenuOpen : true;
8043
+ }
8044
+ if (onclick && !disabled) {
8045
+ onclick(e);
8046
+ }
8047
+ };
8048
+ // Get internal props passed from parent Sidenav
8049
+ const isExpanded = attrs._isExpanded !== false;
8050
+ const position = attrs._position || 'left';
8051
+ const isRightAligned = position === 'right';
8052
+ // In expanded mode, icons are at the outside edge
8053
+ // In collapsed mode, icons are centered
8054
+ const content = isRightAligned
8055
+ ? [
8056
+ // Right-aligned: text on left, icon on right
8057
+ isExpanded && m('span.sidenav-item-text', { style: { 'flex': '1', 'text-align': 'left', 'margin-right': '8px' } }, text || children),
8058
+ m('i.material-icons', { style: { 'min-width': '24px', 'width': '24px' } }, icon || ''),
8059
+ ]
8060
+ : [
8061
+ // Left-aligned: icon on left, text on right
8062
+ m('i.material-icons', { style: { 'min-width': '24px', 'width': '24px' } }, icon || ''),
8063
+ isExpanded && m('span.sidenav-item-text', { style: { 'margin-left': '8px', 'flex': '1' } }, text || children),
8064
+ ];
8065
+ const linkStyle = {
8066
+ display: 'flex',
8067
+ 'align-items': 'center',
8068
+ padding: isExpanded ? '12px 16px' : '12px 18px',
8069
+ 'justify-content': isExpanded ? (isRightAligned ? 'flex-end' : 'flex-start') : 'center',
8070
+ };
8071
+ const mainItem = href && !disabled
8072
+ ? m('li', { class: itemClasses }, [
7882
8073
  m('a', {
7883
8074
  href,
7884
- onclick: disabled ? undefined : onclick,
8075
+ onclick: handleMainClick,
8076
+ style: linkStyle,
8077
+ }, content),
8078
+ ])
8079
+ : m('li', { class: itemClasses }, [
8080
+ m('a', {
8081
+ onclick: handleMainClick,
8082
+ href: '#!',
8083
+ style: linkStyle,
7885
8084
  }, content),
7886
8085
  ]);
8086
+ // Return main item with submenu if applicable
8087
+ if (hasSubmenu && active && isSubmenuOpen) {
8088
+ return [
8089
+ mainItem,
8090
+ submenu.map((subItem) => m(NavbarSubItem, Object.assign(Object.assign({}, subItem), { mode: submenuMode, isExpanded,
8091
+ position }))),
8092
+ ];
7887
8093
  }
7888
- return m('li', { class: itemClasses }, [
7889
- m('a', {
7890
- onclick: disabled ? undefined : onclick,
7891
- href: '#!',
7892
- }, content),
7893
- ]);
8094
+ return mainItem;
7894
8095
  },
7895
8096
  };
7896
8097
  };