mithril-materialized 3.12.0 → 3.14.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
@@ -622,6 +622,85 @@
622
622
  },
623
623
  });
624
624
 
625
+ const iconPaths = {
626
+ caret: [
627
+ 'M7 10l5 5 5-5z', // arrow
628
+ 'M0 0h24v24H0z', // background
629
+ ],
630
+ close: [
631
+ 'M18.3 5.71a1 1 0 0 0-1.41 0L12 10.59 7.11 5.7A1 1 0 0 0 5.7 7.11L10.59 12l-4.89 4.89a1 1 0 1 0 1.41 1.41L12 13.41l4.89 4.89a1 1 0 0 0 1.41-1.41L13.41 12l4.89-4.89a1 1 0 0 0 0-1.4z',
632
+ 'M0 0h24v24H0z',
633
+ ],
634
+ chevron: [
635
+ 'M16.59 8.59L12 13.17 7.41 8.59 6 10l6 6 6-6z', // chevron down
636
+ 'M0 0h24v24H0z', // background
637
+ ],
638
+ chevron_left: [
639
+ 'M15.41 7.41L14 6l-6 6 6 6 1.41-1.41L10.83 12z', // chevron left
640
+ 'M0 0h24v24H0z', // background
641
+ ],
642
+ chevron_right: [
643
+ 'M10 6L8.59 7.41 13.17 12l-4.58 4.59L10 18l6-6z', // chevron right
644
+ 'M0 0h24v24H0z', // background
645
+ ],
646
+ menu: [
647
+ 'M3 18h18v-2H3v2zm0-5h18v-2H3v2zm0-7v2h18V6H3z', // hamburger menu
648
+ 'M0 0h24v24H0z', // background
649
+ ],
650
+ expand: [
651
+ 'M19 13h-6v6h-2v-6H5v-2h6V5h2v6h6v2z', // plus
652
+ 'M0 0h24v24H0z', // background
653
+ ],
654
+ collapse: [
655
+ 'M19 13H5v-2h14v2z', // minus
656
+ 'M0 0h24v24H0z', // background
657
+ ],
658
+ check: [
659
+ 'M9 16.17L4.83 12l-1.42 1.41L9 19 21 7l-1.41-1.41z', // checkmark
660
+ 'M0 0h24v24H0z', // background
661
+ ],
662
+ radio_checked: [
663
+ '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
664
+ 'M0 0h24v24H0z', // background
665
+ ],
666
+ radio_unchecked: [
667
+ '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
668
+ 'M0 0h24v24H0z', // background
669
+ ],
670
+ light_mode: [
671
+ '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',
672
+ 'M0 0h24v24H0z', // background
673
+ ],
674
+ dark_mode: [
675
+ 'M12 3a9 9 0 1 0 9 9c0-.46-.04-.92-.1-1.36a5.39 5.39 0 0 1-4.4 2.26 5.4 5.4 0 0 1-3.14-9.8c-.44-.06-.9-.1-1.36-.1z',
676
+ 'M0 0h24v24H0z', // background
677
+ ],
678
+ delete: [
679
+ 'M6 19c0 1.1.9 2 2 2h8c1.1 0 2-.9 2-2V7H6v12zM19 4h-3.5l-1-1h-5l-1 1H5v2h14V4z',
680
+ 'M0 0h24v24H0z', // background
681
+ ],
682
+ };
683
+ const MaterialIcon = () => {
684
+ return {
685
+ view: ({ attrs }) => {
686
+ var _a;
687
+ const { name, direction = 'down', style } = attrs, props = __rest(attrs, ["name", "direction", "style"]);
688
+ const rotationMap = {
689
+ down: 0,
690
+ up: 180,
691
+ left: 90,
692
+ right: -90,
693
+ };
694
+ const rotation = (_a = rotationMap[direction]) !== null && _a !== void 0 ? _a : 0;
695
+ const transform = rotation ? `rotate(${rotation}deg)` : undefined;
696
+ return m('svg', Object.assign(Object.assign({}, props), { style: Object.assign({ transform }, style), height: '24px', width: '24px', viewBox: '0 0 24 24', xmlns: 'http://www.w3.org/2000/svg' }), iconPaths[name].map((d) => m('path', {
697
+ d,
698
+ fill: d.includes('M0 0h24v24H0z') ? 'none' : 'currentColor',
699
+ })));
700
+ },
701
+ };
702
+ };
703
+
625
704
  /*!
626
705
  * Waves Effect for Mithril Materialized
627
706
  * Based on Waves v0.6.4 by Alfiana E. Sibuea
@@ -727,7 +806,7 @@
727
806
  const ButtonFactory = (element, defaultClassNames, type = '') => {
728
807
  return () => {
729
808
  return {
730
- view: ({ attrs }) => {
809
+ view: ({ attrs, children }) => {
731
810
  const { tooltip, tooltipPosition, tooltipPostion, // Keep for backwards compatibility
732
811
  iconName, iconClass, label, className, variant } = attrs, params = __rest(attrs, ["tooltip", "tooltipPosition", "tooltipPostion", "iconName", "iconClass", "label", "className", "variant"]);
733
812
  // Use variant or fallback to factory type
@@ -743,7 +822,7 @@
743
822
  ontouchstart: WavesEffect.onTouchStart,
744
823
  ontouchend: WavesEffect.onTouchEnd
745
824
  } : {};
746
- return m(element, Object.assign(Object.assign(Object.assign({}, params), wavesHandlers), { className: cn, 'data-position': tooltip ? position : undefined, 'data-tooltip': tooltip || undefined, type: buttonType }), iconName ? m(Icon, { iconName, className: iconClass || 'left' }) : undefined, label ? label : undefined);
825
+ return m(element, Object.assign(Object.assign(Object.assign({}, params), wavesHandlers), { className: cn, 'data-position': tooltip ? position : undefined, 'data-tooltip': tooltip || undefined, type: buttonType }), iconName ? m(Icon, { iconName, className: iconClass || 'left' }) : undefined, label ? label : undefined, children);
747
826
  },
748
827
  };
749
828
  };
@@ -755,6 +834,54 @@
755
834
  const IconButton = ButtonFactory('button', 'btn-flat btn-icon waves-effect waves-teal', 'button');
756
835
  const RoundIconButton = ButtonFactory('button', 'btn-floating btn-large waves-effect waves-light', 'button');
757
836
  const SubmitButton = ButtonFactory('button', 'btn waves-effect waves-light', 'submit');
837
+ const RaisedIconButton = ButtonFactory('button', 'btn waves-effect waves-light', 'button');
838
+ const ConfirmButton = () => {
839
+ let isConfirming = false;
840
+ let isBlocked = false;
841
+ let timeoutId;
842
+ let blockTimeoutId;
843
+ const reset = () => {
844
+ isConfirming = false;
845
+ isBlocked = false;
846
+ m.redraw();
847
+ };
848
+ const unblock = () => {
849
+ isBlocked = false;
850
+ };
851
+ return {
852
+ onremove: () => {
853
+ window.clearTimeout(timeoutId);
854
+ window.clearTimeout(blockTimeoutId);
855
+ },
856
+ view: ({ attrs }) => {
857
+ const { iconName = 'delete', confirmIconName = 'check', confirmColor = 'red', timeout = 3000, clickDelay = 500, onFirstClick, onclick } = attrs, props = __rest(attrs, ["iconName", "confirmIconName", "confirmColor", "timeout", "clickDelay", "onFirstClick", "onclick"]);
858
+ const handleClick = (e) => {
859
+ e.preventDefault();
860
+ if (isBlocked)
861
+ return;
862
+ if (isConfirming) {
863
+ window.clearTimeout(timeoutId);
864
+ window.clearTimeout(blockTimeoutId); // Clean up safety
865
+ isConfirming = false;
866
+ onclick === null || onclick === void 0 ? void 0 : onclick(e);
867
+ }
868
+ else {
869
+ isConfirming = true;
870
+ isBlocked = true;
871
+ onFirstClick === null || onFirstClick === void 0 ? void 0 : onFirstClick();
872
+ timeoutId = window.setTimeout(reset, timeout);
873
+ blockTimeoutId = window.setTimeout(unblock, clickDelay);
874
+ }
875
+ };
876
+ const cn = isConfirming ? confirmColor : 'red-text';
877
+ const commonProps = Object.assign(Object.assign({}, props), { className: `${props.className || ''} ${cn}`, style: Object.assign(Object.assign(Object.assign({}, props.style), { display: 'flex', alignItems: 'center', justifyContent: 'center' }), (isConfirming ? { padding: '0 8px', width: 'auto', minWidth: 'auto' } : {})), onclick: handleClick });
878
+ if (isConfirming) {
879
+ return m(RaisedIconButton, commonProps, m(MaterialIcon, { name: confirmIconName }));
880
+ }
881
+ return m(IconButton, commonProps, m(MaterialIcon, { name: iconName }));
882
+ },
883
+ };
884
+ };
758
885
 
759
886
  /**
760
887
  * Materialize CSS Carousel component with dynamic positioning
@@ -1146,81 +1273,6 @@
1146
1273
  };
1147
1274
  };
1148
1275
 
1149
- const iconPaths = {
1150
- caret: [
1151
- 'M7 10l5 5 5-5z', // arrow
1152
- 'M0 0h24v24H0z', // background
1153
- ],
1154
- close: [
1155
- 'M18.3 5.71a1 1 0 0 0-1.41 0L12 10.59 7.11 5.7A1 1 0 0 0 5.7 7.11L10.59 12l-4.89 4.89a1 1 0 1 0 1.41 1.41L12 13.41l4.89 4.89a1 1 0 0 0 1.41-1.41L13.41 12l4.89-4.89a1 1 0 0 0 0-1.4z',
1156
- 'M0 0h24v24H0z',
1157
- ],
1158
- chevron: [
1159
- 'M16.59 8.59L12 13.17 7.41 8.59 6 10l6 6 6-6z', // chevron down
1160
- 'M0 0h24v24H0z', // background
1161
- ],
1162
- chevron_left: [
1163
- 'M15.41 7.41L14 6l-6 6 6 6 1.41-1.41L10.83 12z', // chevron left
1164
- 'M0 0h24v24H0z', // background
1165
- ],
1166
- chevron_right: [
1167
- 'M10 6L8.59 7.41 13.17 12l-4.58 4.59L10 18l6-6z', // chevron right
1168
- 'M0 0h24v24H0z', // background
1169
- ],
1170
- menu: [
1171
- 'M3 18h18v-2H3v2zm0-5h18v-2H3v2zm0-7v2h18V6H3z', // hamburger menu
1172
- 'M0 0h24v24H0z', // background
1173
- ],
1174
- expand: [
1175
- 'M19 13h-6v6h-2v-6H5v-2h6V5h2v6h6v2z', // plus
1176
- 'M0 0h24v24H0z', // background
1177
- ],
1178
- collapse: [
1179
- 'M19 13H5v-2h14v2z', // minus
1180
- 'M0 0h24v24H0z', // background
1181
- ],
1182
- check: [
1183
- 'M9 16.17L4.83 12l-1.42 1.41L9 19 21 7l-1.41-1.41z', // checkmark
1184
- 'M0 0h24v24H0z', // background
1185
- ],
1186
- radio_checked: [
1187
- '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
1188
- 'M0 0h24v24H0z', // background
1189
- ],
1190
- radio_unchecked: [
1191
- '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
1192
- 'M0 0h24v24H0z', // background
1193
- ],
1194
- light_mode: [
1195
- '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',
1196
- 'M0 0h24v24H0z', // background
1197
- ],
1198
- dark_mode: [
1199
- 'M12 3a9 9 0 1 0 9 9c0-.46-.04-.92-.1-1.36a5.39 5.39 0 0 1-4.4 2.26 5.4 5.4 0 0 1-3.14-9.8c-.44-.06-.9-.1-1.36-.1z',
1200
- 'M0 0h24v24H0z', // background
1201
- ],
1202
- };
1203
- const MaterialIcon = () => {
1204
- return {
1205
- view: ({ attrs }) => {
1206
- var _a;
1207
- const { name, direction = 'down', style } = attrs, props = __rest(attrs, ["name", "direction", "style"]);
1208
- const rotationMap = {
1209
- down: 0,
1210
- up: 180,
1211
- left: 90,
1212
- right: -90,
1213
- };
1214
- const rotation = (_a = rotationMap[direction]) !== null && _a !== void 0 ? _a : 0;
1215
- const transform = rotation ? `rotate(${rotation}deg)` : undefined;
1216
- return m('svg', Object.assign(Object.assign({}, props), { style: Object.assign({ transform }, style), height: '24px', width: '24px', viewBox: '0 0 24 24', xmlns: 'http://www.w3.org/2000/svg' }), iconPaths[name].map((d) => m('path', {
1217
- d,
1218
- fill: d.includes('M0 0h24v24H0z') ? 'none' : 'currentColor',
1219
- })));
1220
- },
1221
- };
1222
- };
1223
-
1224
1276
  const Chips = () => {
1225
1277
  const state = {
1226
1278
  chipsData: [],
@@ -1639,7 +1691,7 @@
1639
1691
  const ListItem = () => {
1640
1692
  return {
1641
1693
  view: ({ attrs: { item, mode } }) => {
1642
- const { title, content = '', active, iconName, avatar, className, onclick } = item;
1694
+ const { title, content, active, iconName, avatar, className, onclick } = item;
1643
1695
  return mode === exports.CollectionMode.AVATAR
1644
1696
  ? m('li.collection-item.avatar', {
1645
1697
  className: active ? 'active' : '',
@@ -1649,12 +1701,21 @@
1649
1701
  ? m('img.circle', { src: avatar })
1650
1702
  : m('i.material-icons.circle', { className }, avatar),
1651
1703
  m('span.title', title),
1652
- m('p', m.trust(content)),
1653
- m(SecondaryContent, item),
1654
- ])
1704
+ content ? (typeof content === 'string' ? m('p', m.trust(content)) : m('p', content)) : undefined,
1705
+ iconName ? m(SecondaryContent, item) : undefined,
1706
+ ].filter(Boolean))
1655
1707
  : m('li.collection-item', {
1656
1708
  className: active ? 'active' : '',
1657
- }, iconName ? m('div', [title, m(SecondaryContent, item)]) : title);
1709
+ onclick: onclick ? () => onclick(item) : undefined,
1710
+ }, content
1711
+ ? m('div', [
1712
+ m('div', title),
1713
+ typeof content === 'string' ? m('p.secondary-text', content) : content,
1714
+ iconName ? m(SecondaryContent, item) : undefined,
1715
+ ].filter(Boolean))
1716
+ : iconName
1717
+ ? m('div', [title, m(SecondaryContent, item)])
1718
+ : title);
1658
1719
  },
1659
1720
  };
1660
1721
  };
@@ -10755,12 +10816,12 @@
10755
10816
  const RatingItem = () => {
10756
10817
  return {
10757
10818
  view: ({ attrs }) => {
10758
- const { index, displayValue, step, icons, allowHalfSteps, disabled, onclick, onmouseover } = attrs;
10819
+ const { index, displayValue, step, icons, allowHalfSteps, disabled, showTooltip, tooltipLabel, onclick, onmouseover, } = attrs;
10759
10820
  const itemValue = (index + 1) * step;
10760
10821
  // Calculate fill state based on displayValue vs itemValue
10761
10822
  const diff = displayValue - itemValue;
10762
10823
  const fillState = diff >= 0 ? 'full' : allowHalfSteps && diff >= -step / 2 ? 'half' : 'empty';
10763
- return m('.rating__item', {
10824
+ return m('.rating__item.no-select', {
10764
10825
  className: [
10765
10826
  fillState === 'full' ? 'rating__item--filled' : '',
10766
10827
  fillState === 'half' ? 'rating__item--half' : '',
@@ -10781,6 +10842,8 @@
10781
10842
  clipPath: fillState === 'half' ? 'inset(0 50% 0 0)' : undefined,
10782
10843
  },
10783
10844
  }, typeof icons.filled === 'string' ? icons.filled : m(icons.filled)),
10845
+ // Tooltip
10846
+ showTooltip && tooltipLabel && m('.rating__tooltip', tooltipLabel),
10784
10847
  ]);
10785
10848
  },
10786
10849
  };
@@ -10852,6 +10915,7 @@
10852
10915
  },
10853
10916
  // Array.from({ length: itemCount }, (_, i) => renderRatingItem(attrs, i))
10854
10917
  [...Array(itemCount)].map((_, i) => {
10918
+ var _a;
10855
10919
  const itemValue = (i + 1) * step;
10856
10920
  return m(RatingItem, {
10857
10921
  key: `rating-item-${i}`,
@@ -10861,6 +10925,8 @@
10861
10925
  icons: Object.assign(Object.assign({}, DEFAULT_ICONS), attrs.icon),
10862
10926
  allowHalfSteps: attrs.allowHalfSteps,
10863
10927
  disabled: attrs.disabled,
10928
+ showTooltip: attrs.showTooltips,
10929
+ tooltipLabel: (_a = attrs.tooltipLabels) === null || _a === void 0 ? void 0 : _a[i],
10864
10930
  onclick: () => handleItemClick(attrs, itemValue),
10865
10931
  onmouseover: () => handleItemHover(attrs, itemValue),
10866
10932
  });
@@ -10876,6 +10942,237 @@
10876
10942
  };
10877
10943
  };
10878
10944
 
10945
+ /** Create a LikertScale component */
10946
+ const LikertScale = () => {
10947
+ const state = {
10948
+ id: uniqueId(),
10949
+ groupId: uniqueId(),
10950
+ internalValue: undefined,
10951
+ isFocused: false,
10952
+ };
10953
+ const isControlled = (attrs) => typeof attrs.value !== 'undefined' && typeof attrs.onchange === 'function';
10954
+ const getCurrentValue = (attrs) => {
10955
+ var _a, _b;
10956
+ const controlled = isControlled(attrs);
10957
+ const isNonInteractive = attrs.readonly || attrs.disabled;
10958
+ if (controlled) {
10959
+ return attrs.value;
10960
+ }
10961
+ // Non-interactive components: prefer defaultValue, fallback to value
10962
+ if (isNonInteractive) {
10963
+ return (_a = attrs.defaultValue) !== null && _a !== void 0 ? _a : attrs.value;
10964
+ }
10965
+ // Interactive uncontrolled: use internal state (user can change it)
10966
+ return (_b = state.internalValue) !== null && _b !== void 0 ? _b : attrs.defaultValue;
10967
+ };
10968
+ const getLabelText = (value, min, max, getLabelFn) => {
10969
+ if (getLabelFn && value !== undefined) {
10970
+ return getLabelFn(value, min, max);
10971
+ }
10972
+ if (value === undefined) {
10973
+ return `No selection, please choose a value between ${min} and ${max}`;
10974
+ }
10975
+ return `Selected ${value} out of ${min} to ${max}`;
10976
+ };
10977
+ const getSizeClass = (size = 'medium') => {
10978
+ switch (size) {
10979
+ case 'small':
10980
+ return 'likert-scale--small';
10981
+ case 'large':
10982
+ return 'likert-scale--large';
10983
+ default:
10984
+ return 'likert-scale--medium';
10985
+ }
10986
+ };
10987
+ const getDensityClass = (density = 'standard') => {
10988
+ switch (density) {
10989
+ case 'compact':
10990
+ return 'likert-scale--compact';
10991
+ case 'comfortable':
10992
+ return 'likert-scale--comfortable';
10993
+ default:
10994
+ return 'likert-scale--standard';
10995
+ }
10996
+ };
10997
+ const getLayoutClass = (layout = 'responsive') => {
10998
+ switch (layout) {
10999
+ case 'horizontal':
11000
+ return 'likert-scale--horizontal';
11001
+ case 'vertical':
11002
+ return 'likert-scale--vertical';
11003
+ default:
11004
+ return 'likert-scale--responsive';
11005
+ }
11006
+ };
11007
+ const handleChange = (attrs, newValue) => {
11008
+ var _a;
11009
+ if (attrs.readonly || attrs.disabled)
11010
+ return;
11011
+ if (!isControlled(attrs)) {
11012
+ state.internalValue = newValue;
11013
+ }
11014
+ (_a = attrs.onchange) === null || _a === void 0 ? void 0 : _a.call(attrs, newValue);
11015
+ };
11016
+ const handleKeyDown = (attrs, e) => {
11017
+ if (attrs.readonly || attrs.disabled)
11018
+ return;
11019
+ const min = attrs.min || 1;
11020
+ const max = attrs.max || 5;
11021
+ const step = attrs.step || 1;
11022
+ const currentValue = getCurrentValue(attrs);
11023
+ let newValue = currentValue;
11024
+ switch (e.key) {
11025
+ case 'ArrowRight':
11026
+ case 'ArrowUp':
11027
+ e.preventDefault();
11028
+ newValue = currentValue !== undefined ? Math.min(max, currentValue + step) : min;
11029
+ break;
11030
+ case 'ArrowLeft':
11031
+ case 'ArrowDown':
11032
+ e.preventDefault();
11033
+ newValue = currentValue !== undefined ? Math.max(min, currentValue - step) : min;
11034
+ break;
11035
+ case 'Home':
11036
+ e.preventDefault();
11037
+ newValue = min;
11038
+ break;
11039
+ case 'End':
11040
+ e.preventDefault();
11041
+ newValue = max;
11042
+ break;
11043
+ default:
11044
+ return;
11045
+ }
11046
+ if (newValue !== currentValue) {
11047
+ handleChange(attrs, newValue);
11048
+ }
11049
+ };
11050
+ const LikertScaleItem = () => {
11051
+ return {
11052
+ view: ({ attrs }) => {
11053
+ const { value, currentValue, showNumber, showTooltip, tooltipLabel, groupId, name, disabled, readonly, onchange, } = attrs;
11054
+ const radioId = `${groupId}-${value}`;
11055
+ const isChecked = currentValue === value;
11056
+ return m('.likert-scale__item.no-select', {
11057
+ className: [
11058
+ isChecked ? 'likert-scale__item--checked' : '',
11059
+ disabled ? 'likert-scale__item--disabled' : '',
11060
+ readonly ? 'likert-scale__item--readonly' : '',
11061
+ ]
11062
+ .filter(Boolean)
11063
+ .join(' '),
11064
+ }, [
11065
+ // Number label (optional)
11066
+ showNumber && m('.likert-scale__number', value),
11067
+ // Radio button input
11068
+ m('input[type=radio].likert-scale__input', {
11069
+ id: radioId,
11070
+ name: name || groupId,
11071
+ value: value,
11072
+ checked: isChecked,
11073
+ disabled: disabled || readonly,
11074
+ onchange: () => onchange(value),
11075
+ }),
11076
+ // Label for radio button
11077
+ m('label.likert-scale__label', {
11078
+ for: radioId,
11079
+ }),
11080
+ // Tooltip (optional)
11081
+ showTooltip && tooltipLabel && m('.likert-scale__tooltip', tooltipLabel),
11082
+ ]);
11083
+ },
11084
+ };
11085
+ };
11086
+ return {
11087
+ oninit: ({ attrs }) => {
11088
+ const controlled = isControlled(attrs);
11089
+ const isNonInteractive = attrs.readonly || attrs.disabled;
11090
+ // Warn developer for improper controlled usage
11091
+ if (attrs.value !== undefined && !controlled && !isNonInteractive) {
11092
+ console.warn(`LikertScale component received 'value' prop without 'onchange' handler. ` +
11093
+ `Use 'defaultValue' for uncontrolled components or add 'onchange' for controlled components.`);
11094
+ }
11095
+ if (!controlled) {
11096
+ state.internalValue = attrs.defaultValue;
11097
+ }
11098
+ },
11099
+ view: ({ attrs }) => {
11100
+ const { min = 1, max = 5, step = 1, size = 'medium', density = 'standard', layout = 'responsive', className = '', style = {}, readonly = false, disabled = false, id = state.id, name, label, description, isMandatory, startLabel, middleLabel, endLabel, showNumbers = false, showTooltips = false, tooltipLabels, alignLabels = false } = attrs, ariaAttrs = __rest(attrs, ["min", "max", "step", "size", "density", "layout", "className", "style", "readonly", "disabled", "id", "name", "label", "description", "isMandatory", "startLabel", "middleLabel", "endLabel", "showNumbers", "showTooltips", "tooltipLabels", "alignLabels"]);
11101
+ const currentValue = getCurrentValue(attrs);
11102
+ const itemCount = Math.floor((max - min) / step) + 1;
11103
+ // Generate scale values
11104
+ const scaleValues = Array.from({ length: itemCount }, (_, i) => min + i * step);
11105
+ return m('.likert-scale', {
11106
+ className: [
11107
+ 'likert-scale',
11108
+ getSizeClass(size),
11109
+ getDensityClass(density),
11110
+ getLayoutClass(layout),
11111
+ readonly ? 'likert-scale--readonly' : '',
11112
+ disabled ? 'likert-scale--disabled' : '',
11113
+ state.isFocused ? 'likert-scale--focused' : '',
11114
+ alignLabels ? 'likert-scale--aligned' : '',
11115
+ className,
11116
+ ]
11117
+ .filter(Boolean)
11118
+ .join(' '),
11119
+ style,
11120
+ id,
11121
+ role: 'radiogroup',
11122
+ 'aria-label': ariaAttrs['aria-label'] || attrs.ariaLabel || label || `Rating scale from ${min} to ${max}`,
11123
+ 'aria-labelledby': ariaAttrs['aria-labelledby'],
11124
+ 'aria-readonly': readonly,
11125
+ 'aria-disabled': disabled,
11126
+ onkeydown: (e) => handleKeyDown(attrs, e),
11127
+ onfocus: () => {
11128
+ state.isFocused = true;
11129
+ },
11130
+ onblur: () => {
11131
+ state.isFocused = false;
11132
+ },
11133
+ tabindex: readonly || disabled ? -1 : 0,
11134
+ }, [
11135
+ // Label section (only text label, not the description)
11136
+ label &&
11137
+ m('.likert-scale__question-label', [
11138
+ m('span', label + (isMandatory ? ' *' : '')),
11139
+ description && m('.likert-scale__description', m.trust(description)),
11140
+ ]),
11141
+ // Scale section container
11142
+ m('.likert-scale__scale-container', [
11143
+ // Scale items with numbers
11144
+ m('.likert-scale__scale', scaleValues.map((value) => m(LikertScaleItem, {
11145
+ key: `likert-item-${value}`,
11146
+ value,
11147
+ currentValue,
11148
+ showNumber: showNumbers,
11149
+ showTooltip: showTooltips,
11150
+ tooltipLabel: tooltipLabels === null || tooltipLabels === void 0 ? void 0 : tooltipLabels[value - min],
11151
+ groupId: state.groupId,
11152
+ name,
11153
+ disabled,
11154
+ readonly,
11155
+ onchange: (v) => handleChange(attrs, v),
11156
+ }))),
11157
+ // Scale anchors
11158
+ (startLabel || middleLabel || endLabel) &&
11159
+ m('.likert-scale__anchors', [
11160
+ startLabel && m('.likert-scale__anchor.likert-scale__anchor--start', startLabel),
11161
+ middleLabel && m('.likert-scale__anchor.likert-scale__anchor--middle', middleLabel),
11162
+ endLabel && m('.likert-scale__anchor.likert-scale__anchor--end', endLabel),
11163
+ ]),
11164
+ ]),
11165
+ // Screen reader text
11166
+ m('.likert-scale__sr-only', {
11167
+ className: 'likert-scale__sr-only',
11168
+ 'aria-live': 'polite',
11169
+ 'aria-atomic': 'true',
11170
+ }, getLabelText(currentValue, min, max, attrs.getLabelText)),
11171
+ ]);
11172
+ },
11173
+ };
11174
+ };
11175
+
10879
11176
  /**
10880
11177
  * ToggleButton component.
10881
11178
  *
@@ -11210,6 +11507,7 @@
11210
11507
  exports.CollapsibleItem = CollapsibleItem;
11211
11508
  exports.Collection = Collection;
11212
11509
  exports.ColorInput = ColorInput;
11510
+ exports.ConfirmButton = ConfirmButton;
11213
11511
  exports.DataTable = DataTable;
11214
11512
  exports.DatePicker = DatePicker;
11215
11513
  exports.DigitalClock = DigitalClock;
@@ -11227,6 +11525,7 @@
11227
11525
  exports.InputCheckbox = InputCheckbox;
11228
11526
  exports.Label = Label;
11229
11527
  exports.LargeButton = LargeButton;
11528
+ exports.LikertScale = LikertScale;
11230
11529
  exports.LinearProgress = LinearProgress;
11231
11530
  exports.ListItem = ListItem;
11232
11531
  exports.Mandatory = Mandatory;
@@ -0,0 +1,70 @@
1
+ import { FactoryComponent, Attributes } from 'mithril';
2
+ /** Likert scale component size options */
3
+ export type LikertScaleSize = 'small' | 'medium' | 'large';
4
+ /** Likert scale component density options */
5
+ export type LikertScaleDensity = 'compact' | 'standard' | 'comfortable';
6
+ /** Likert scale layout options */
7
+ export type LikertScaleLayout = 'horizontal' | 'vertical' | 'responsive';
8
+ /** Likert scale component attributes */
9
+ export interface LikertScaleAttrs<T extends number = number> extends Attributes {
10
+ /** Minimum scale value (default: 1) */
11
+ min?: number;
12
+ /** Maximum scale value (default: 5) */
13
+ max?: number;
14
+ /** Step size for scale increments (default: 1) */
15
+ step?: number;
16
+ /** Current value for controlled mode */
17
+ value?: T;
18
+ /** Initial value for uncontrolled mode */
19
+ defaultValue?: T;
20
+ /** Callback when value changes */
21
+ onchange?: (value: T) => void;
22
+ /** Question/prompt text */
23
+ label?: string;
24
+ /** Helper text description */
25
+ description?: string;
26
+ /** Anchor label for minimum value (e.g., "Very Unhappy") */
27
+ startLabel?: string;
28
+ /** Anchor label for middle value (optional, e.g., "Neutral") */
29
+ middleLabel?: string;
30
+ /** Anchor label for maximum value (e.g., "Very Happy") */
31
+ endLabel?: string;
32
+ /** Whether to display numeric values (default: false) */
33
+ showNumbers?: boolean;
34
+ /** Whether to show tooltips on hover (default: false) */
35
+ showTooltips?: boolean;
36
+ /** Custom tooltip labels for each value */
37
+ tooltipLabels?: string[];
38
+ /** Density variant (default: 'standard') */
39
+ density?: LikertScaleDensity;
40
+ /** Size variant (default: 'medium') */
41
+ size?: LikertScaleSize;
42
+ /** Layout mode (default: 'responsive' = horizontal on desktop, vertical on mobile) */
43
+ layout?: LikertScaleLayout;
44
+ /** HTML ID for the component */
45
+ id?: string;
46
+ /** Name for form submission */
47
+ name?: string;
48
+ /** Whether the component is disabled */
49
+ disabled?: boolean;
50
+ /** Whether the component is read-only */
51
+ readonly?: boolean;
52
+ /** If true, add a mandatory '*' after the label */
53
+ isMandatory?: boolean;
54
+ /** ARIA label for the component */
55
+ 'aria-label'?: string;
56
+ /** ARIA label for the component (camelCase alternative) */
57
+ ariaLabel?: string;
58
+ /** ARIA labelledby reference */
59
+ 'aria-labelledby'?: string;
60
+ /** Function to get label text for accessibility */
61
+ getLabelText?: (value: number, min: number, max: number) => string;
62
+ /** Class name for the container */
63
+ className?: string;
64
+ /** Additional CSS styles */
65
+ style?: any;
66
+ /** Use CSS grid for label/scale alignment in multi-question surveys (default: false) */
67
+ alignLabels?: boolean;
68
+ }
69
+ /** Create a LikertScale component */
70
+ export declare const LikertScale: FactoryComponent<LikertScaleAttrs>;
@@ -13,8 +13,9 @@ declare const iconPaths: {
13
13
  readonly radio_unchecked: readonly ["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", "M0 0h24v24H0z"];
14
14
  readonly light_mode: readonly ["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", "M0 0h24v24H0z"];
15
15
  readonly dark_mode: readonly ["M12 3a9 9 0 1 0 9 9c0-.46-.04-.92-.1-1.36a5.39 5.39 0 0 1-4.4 2.26 5.4 5.4 0 0 1-3.14-9.8c-.44-.06-.9-.1-1.36-.1z", "M0 0h24v24H0z"];
16
+ readonly delete: readonly ["M6 19c0 1.1.9 2 2 2h8c1.1 0 2-.9 2-2V7H6v12zM19 4h-3.5l-1-1h-5l-1 1H5v2h14V4z", "M0 0h24v24H0z"];
16
17
  };
17
- type IconName = keyof typeof iconPaths;
18
+ export type IconName = keyof typeof iconPaths;
18
19
  export interface MaterialIconAttrs extends Attributes {
19
20
  name: IconName;
20
21
  direction?: 'up' | 'down' | 'left' | 'right';
@@ -2778,6 +2778,11 @@ td, th {
2778
2778
  .collection .collection-item.active .secondary-content {
2779
2779
  color: var(--mm-text-on-primary, #fff);
2780
2780
  }
2781
+ .collection .collection-item .secondary-text {
2782
+ color: var(--mm-text-secondary, rgba(0, 0, 0, 0.54));
2783
+ font-size: 0.9rem;
2784
+ margin-top: 4px;
2785
+ }
2781
2786
  .collection a.collection-item {
2782
2787
  display: block;
2783
2788
  transition: 0.25s;