mithril-materialized 3.16.0 → 3.17.1

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.
Files changed (46) hide show
  1. package/README.md +32 -2
  2. package/dist/advanced.css +92 -1
  3. package/dist/button.d.ts +3 -1
  4. package/dist/circular-progress.d.ts +2 -2
  5. package/dist/combobox.d.ts +35 -0
  6. package/dist/components.css +127 -14
  7. package/dist/core.css +278 -3
  8. package/dist/floating-action-button.d.ts +2 -1
  9. package/dist/form-section.d.ts +35 -0
  10. package/dist/forms.css +280 -5
  11. package/dist/index.css +340 -19
  12. package/dist/index.d.ts +3 -2
  13. package/dist/index.esm.js +698 -296
  14. package/dist/index.esm.js.map +1 -0
  15. package/dist/index.js +711 -295
  16. package/dist/index.js.map +1 -0
  17. package/dist/index.min.css +2 -2
  18. package/dist/index.umd.js +711 -295
  19. package/dist/index.umd.js.map +1 -0
  20. package/dist/input-options.d.ts +4 -1
  21. package/dist/likert-scale.d.ts +2 -1
  22. package/dist/linear-progress.d.ts +2 -2
  23. package/dist/material-icon.d.ts +2 -1
  24. package/dist/radio.d.ts +9 -0
  25. package/dist/range-slider.d.ts +3 -2
  26. package/dist/rating.d.ts +2 -1
  27. package/dist/search-select.d.ts +15 -2
  28. package/dist/select.d.ts +14 -1
  29. package/dist/sidenav.d.ts +6 -0
  30. package/dist/treeview.d.ts +2 -2
  31. package/dist/types.d.ts +5 -0
  32. package/dist/utilities.css +93 -2
  33. package/dist/utils.d.ts +30 -1
  34. package/package.json +16 -19
  35. package/sass/components/_badge-component.scss +8 -0
  36. package/sass/components/_buttons.scss +9 -9
  37. package/sass/components/_global.scss +93 -0
  38. package/sass/components/_likert-scale.scss +24 -0
  39. package/sass/components/_theme-variables.scss +22 -1
  40. package/sass/components/_toggle-group.scss +3 -0
  41. package/sass/components/forms/_form-groups.scss +1 -1
  42. package/sass/components/forms/_form-section.scss +63 -0
  43. package/sass/components/forms/_forms.scss +1 -0
  44. package/sass/components/forms/_input-fields.scss +55 -0
  45. package/sass/components/forms/_range-enhanced.scss +3 -3
  46. package/sass/components/forms/_select.scss +85 -0
package/dist/index.umd.js CHANGED
@@ -38,6 +38,35 @@
38
38
  return e.name = "SuppressedError", e.error = error, e.suppressed = suppressed, e;
39
39
  };
40
40
 
41
+ // import './styles/input.css';
42
+ const Mandatory = { view: ({ attrs }) => m('span.mandatory', Object.assign({}, attrs), '*') };
43
+ /** Simple label element, used for most components. */
44
+ const Label = () => {
45
+ return {
46
+ view: (_a) => {
47
+ var _b = _a.attrs, { label, id, isMandatory, isActive, className, initialValue } = _b, params = __rest(_b, ["label", "id", "isMandatory", "isActive", "className", "initialValue"]);
48
+ return label
49
+ ? m('label', Object.assign(Object.assign({}, params), { className: [className, isActive ? 'active' : ''].filter(Boolean).join(' ').trim() || undefined, for: id, oncreate: ({ dom }) => {
50
+ if (!initialValue)
51
+ return;
52
+ const labelEl = dom;
53
+ labelEl.classList.add('active');
54
+ } }), [m.trust(label), isMandatory ? m(Mandatory) : undefined])
55
+ : undefined;
56
+ },
57
+ };
58
+ };
59
+ /** Create a helper text, often used for displaying a small help text. May be replaced by the validation message. */
60
+ const HelperText = () => {
61
+ return {
62
+ view: ({ attrs: { helperText, dataError, dataSuccess, className } }) => {
63
+ return helperText || dataError || dataSuccess
64
+ ? m('span.helper-text.left', { className, 'data-error': dataError, 'data-success': dataSuccess }, dataError ? m.trust(dataError) : dataSuccess ? m.trust(dataSuccess) : helperText ? m.trust(helperText) : '')
65
+ : undefined;
66
+ },
67
+ };
68
+ };
69
+
41
70
  // Utility functions for the library
42
71
  /**
43
72
  * Create a unique ID
@@ -98,6 +127,42 @@
98
127
  * @returns
99
128
  */
100
129
  const padLeft = (n, width = 2, z = '0') => String(n).padStart(width, z);
130
+ const normalizeSelection = (value) => {
131
+ if (value === undefined) {
132
+ return [];
133
+ }
134
+ return Array.isArray(value) ? value : [value];
135
+ };
136
+ const resolveControllableValue = ({ controlled, disabled, controlledValue, defaultValue, internalValue, fallbackValue, }) => {
137
+ var _a, _b;
138
+ if (controlled) {
139
+ return controlledValue !== null && controlledValue !== void 0 ? controlledValue : fallbackValue;
140
+ }
141
+ if (disabled) {
142
+ return (_a = defaultValue !== null && defaultValue !== void 0 ? defaultValue : controlledValue) !== null && _a !== void 0 ? _a : fallbackValue;
143
+ }
144
+ return (_b = internalValue !== null && internalValue !== void 0 ? internalValue : defaultValue) !== null && _b !== void 0 ? _b : fallbackValue;
145
+ };
146
+ const renderFieldChrome = ({ label, id, isMandatory, isActive, initialValue, helperText, dataError, dataSuccess, }) => {
147
+ return [
148
+ label
149
+ ? m(Label, {
150
+ label,
151
+ id,
152
+ isMandatory,
153
+ isActive,
154
+ initialValue,
155
+ })
156
+ : undefined,
157
+ helperText || dataError || dataSuccess
158
+ ? m(HelperText, {
159
+ helperText,
160
+ dataError,
161
+ dataSuccess,
162
+ })
163
+ : undefined,
164
+ ].filter(Boolean);
165
+ };
101
166
  // Keep only essential dropdown positioning styles
102
167
  const getDropdownStyles = (inputRef, overlap = false, options, isDropDown = false) => {
103
168
  if (!inputRef) {
@@ -129,8 +194,10 @@
129
194
  groups.add(option.group);
130
195
  }
131
196
  });
132
- // Calculate total height: options + group headers + padding
133
- estimatedHeight = totalOptions * itemHeight + groups.size * groupHeaderHeight;
197
+ // Match the select dropdown's CSS max-height. Positioning with the full
198
+ // option count would otherwise leave a gap above an input near the bottom
199
+ // of the viewport when the rendered menu is capped at 400px.
200
+ estimatedHeight = Math.min(totalOptions * itemHeight + groups.size * groupHeaderHeight, 400);
134
201
  }
135
202
  const spaceBelow = viewportHeight - rect.bottom;
136
203
  const spaceAbove = rect.top;
@@ -146,33 +213,26 @@
146
213
  const needsScrolling = estimatedHeight > effectiveAvailableSpace;
147
214
  // Calculate the actual height the dropdown will take
148
215
  const actualHeight = needsScrolling ? effectiveAvailableSpace : estimatedHeight;
149
- // Calculate positioning when dropdown should appear above
150
- let topOffset;
151
- if (shouldPositionAbove) {
152
- // Calculate how much space we actually have from top of viewport to top of input
153
- const availableSpaceFromViewportTop = rect.top;
154
- // If dropdown fits comfortably above input, use normal positioning
155
- if (actualHeight <= availableSpaceFromViewportTop) {
156
- topOffset = 12 - actualHeight + (isDropDown ? itemHeight : 0); // Bottom of dropdown aligns with top of input
157
- }
158
- else {
159
- // If dropdown is too tall, position it at the very top of viewport
160
- // This makes the dropdown use all available space from viewport top to input top
161
- topOffset = -availableSpaceFromViewportTop + 5; // 5px margin from viewport top
162
- }
163
- }
164
- else {
165
- topOffset = overlap ? 0 : '100%';
166
- }
167
216
  const styles = {
168
217
  display: 'block',
169
218
  opacity: 1,
170
219
  position: 'absolute',
171
- top: typeof topOffset === 'number' ? `${topOffset}px` : topOffset,
172
220
  left: '0',
173
221
  zIndex: 1000,
174
222
  width: `${rect.width}px`,
175
223
  };
224
+ if (shouldPositionAbove) {
225
+ // This menu is absolutely positioned inside the select wrapper. A negative
226
+ // `top` calculated from viewport coordinates becomes wrong when an ancestor
227
+ // scrolls (as ScenarioSpark's main area does). Anchor the menu to its local
228
+ // containing block instead; its height can then change without detaching it
229
+ // from the input.
230
+ styles.top = 'auto';
231
+ styles.bottom = `${isDropDown ? -18 : inputRef.offsetHeight - 12}px`;
232
+ }
233
+ else {
234
+ styles.top = overlap ? 0 : '100%';
235
+ }
176
236
  // Only add scrolling constraints when necessary
177
237
  if (needsScrolling) {
178
238
  styles.maxHeight = `${actualHeight}px`;
@@ -239,9 +299,17 @@
239
299
  * @param zIndex - Z-index for portal container (default: 1004)
240
300
  */
241
301
  const renderToPortal = (containerId, vnode, zIndex = 1004) => {
242
- const container = getPortalContainer(containerId, zIndex);
302
+ const existingContainer = portalContainers.get(containerId);
303
+ const container = existingContainer ? existingContainer.element : getPortalContainer(containerId, zIndex);
243
304
  m.render(container, vnode);
244
305
  };
306
+ const syncPortalContent = ({ containerId, shouldRender, vnode, zIndex = 1004 }) => {
307
+ if (!shouldRender || vnode === null) {
308
+ clearPortal(containerId);
309
+ return;
310
+ }
311
+ renderToPortal(containerId, vnode, zIndex);
312
+ };
245
313
  /**
246
314
  * Clears portal content and releases container reference.
247
315
  * If this is the last reference, the container will be removed from the DOM.
@@ -256,35 +324,6 @@
256
324
  }
257
325
  };
258
326
 
259
- // import './styles/input.css';
260
- const Mandatory = { view: ({ attrs }) => m('span.mandatory', Object.assign({}, attrs), '*') };
261
- /** Simple label element, used for most components. */
262
- const Label = () => {
263
- return {
264
- view: (_a) => {
265
- var _b = _a.attrs, { label, id, isMandatory, isActive, className, initialValue } = _b, params = __rest(_b, ["label", "id", "isMandatory", "isActive", "className", "initialValue"]);
266
- return label
267
- ? m('label', Object.assign(Object.assign({}, params), { className: [className, isActive ? 'active' : ''].filter(Boolean).join(' ').trim() || undefined, for: id, oncreate: ({ dom }) => {
268
- if (!initialValue)
269
- return;
270
- const labelEl = dom;
271
- labelEl.classList.add('active');
272
- } }), [m.trust(label), isMandatory ? m(Mandatory) : undefined])
273
- : undefined;
274
- },
275
- };
276
- };
277
- /** Create a helper text, often used for displaying a small help text. May be replaced by the validation message. */
278
- const HelperText = () => {
279
- return {
280
- view: ({ attrs: { helperText, dataError, dataSuccess, className } }) => {
281
- return helperText || dataError || dataSuccess
282
- ? m('span.helper-text.left', { className, 'data-error': dataError, 'data-success': dataSuccess }, dataError ? m.trust(dataError) : dataSuccess ? m.trust(dataSuccess) : helperText ? m.trust(helperText) : '')
283
- : undefined;
284
- },
285
- };
286
- };
287
-
288
327
  /** Component to auto complete your text input - Pure Mithril implementation */
289
328
  const Autocomplete = () => {
290
329
  const state = {
@@ -307,6 +346,28 @@
307
346
  .slice(0, limit);
308
347
  return filtered;
309
348
  };
349
+ const highlightMatch = (text, query) => {
350
+ if (!query) {
351
+ return text;
352
+ }
353
+ const lowerText = text.toLowerCase();
354
+ const lowerQuery = query.toLowerCase();
355
+ const nodes = [];
356
+ let start = 0;
357
+ let index = lowerText.indexOf(lowerQuery, start);
358
+ while (index !== -1) {
359
+ if (index > start) {
360
+ nodes.push(text.slice(start, index));
361
+ }
362
+ nodes.push(m('span.highlight', text.slice(index, index + query.length)));
363
+ start = index + query.length;
364
+ index = lowerText.indexOf(lowerQuery, start);
365
+ }
366
+ if (start < text.length) {
367
+ nodes.push(text.slice(start));
368
+ }
369
+ return nodes.length > 0 ? nodes : text;
370
+ };
310
371
  const selectSuggestion = (suggestion, attrs) => {
311
372
  const controlled = isControlled(attrs);
312
373
  // Update internal state for uncontrolled mode
@@ -412,7 +473,6 @@
412
473
  Object.keys(data).some((key) => key.toLowerCase() === currentValue.toLowerCase());
413
474
  // Only open dropdown if there are suggestions and no perfect match
414
475
  state.isOpen = state.suggestions.length > 0 && currentValue.length >= minLength && !hasExactMatch;
415
- const replacer = new RegExp(`(${currentValue})`, 'i');
416
476
  return m('.input-field.autocomplete-wrapper', {
417
477
  className: cn,
418
478
  style,
@@ -493,9 +553,7 @@
493
553
  },
494
554
  }, suggestion.value.replace('icon:', ''))
495
555
  : null,
496
- m('span', suggestion.key
497
- ? m.trust(suggestion.key.replace(replacer, (i) => `<span class="highlight">${i}</span>`))
498
- : ''),
556
+ m('span', suggestion.key ? highlightMatch(suggestion.key, currentValue) : ''),
499
557
  ]))),
500
558
  m(Label, {
501
559
  label,
@@ -722,7 +780,7 @@
722
780
  /**
723
781
  * A factory to create new buttons.
724
782
  *
725
- * @example FlatButton = ButtonFactory('a.waves-effect.waves-teal.btn-flat');
783
+ * @example FlatButton = ButtonFactory('button.waves-effect.waves-teal.btn-flat');
726
784
  */
727
785
  const ButtonFactory = (element, defaultClassNames, type = '') => {
728
786
  return () => {
@@ -745,15 +803,21 @@
745
803
  ontouchend: WavesEffect.onTouchEnd,
746
804
  }
747
805
  : {};
748
- 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 !== undefined ? iconClass : 'left' }) : undefined, label ? label : undefined, children);
806
+ const tagName = element.split(/[.#]/)[0] || element;
807
+ const elementSuffix = element.slice(tagName.length);
808
+ const isLink = Boolean(attrs.href);
809
+ const renderedElement = `${isLink ? 'a' : 'button'}${elementSuffix}`;
810
+ return m(renderedElement, Object.assign(Object.assign(Object.assign({}, params), wavesHandlers), { className: cn, 'data-position': tooltip ? position : undefined, 'data-tooltip': tooltip || undefined,
811
+ // Links retain native navigation semantics and must not have a button type.
812
+ type: isLink ? undefined : buttonType }), iconName ? m(Icon, { iconName, className: iconClass !== undefined ? iconClass : 'left' }) : undefined, label ? label : undefined, children);
749
813
  },
750
814
  };
751
815
  };
752
816
  };
753
- const Button = ButtonFactory('a', 'waves-effect waves-light btn', 'button');
754
- const LargeButton = ButtonFactory('a', 'waves-effect waves-light btn-large', 'button');
755
- const SmallButton = ButtonFactory('a', 'waves-effect waves-light btn-small', 'button');
756
- const FlatButton = ButtonFactory('a', 'waves-effect waves-teal btn-flat', 'button');
817
+ const Button = ButtonFactory('button', 'waves-effect waves-light btn', 'button');
818
+ const LargeButton = ButtonFactory('button', 'waves-effect waves-light btn-large', 'button');
819
+ const SmallButton = ButtonFactory('button', 'waves-effect waves-light btn-small', 'button');
820
+ const FlatButton = ButtonFactory('button', 'waves-effect waves-teal btn-flat', 'button');
757
821
  const IconButton = ButtonFactory('button', 'btn-flat btn-icon waves-effect waves-teal', 'button');
758
822
  const RoundIconButton = ButtonFactory('button', 'btn-floating btn-large waves-effect waves-light', 'button');
759
823
  const SubmitButton = ButtonFactory('button', 'btn waves-effect waves-light', 'submit');
@@ -810,7 +874,7 @@
810
874
  // Add square styling for icon-only confirming state
811
875
  const buttonStyle = !label && isConfirming
812
876
  ? Object.assign(Object.assign({}, props.style), { width: '36px', height: '36px', padding: '0', minWidth: '28px', display: 'inline-flex', alignItems: 'center', justifyContent: 'center', borderRadius: '2px' }) : props.style;
813
- return m(ButtonComponent, Object.assign(Object.assign({}, props), { style: buttonStyle, className: `${props.className || ''} ${cn}`, iconName: currentIconName, iconClass: label ? (iconClass || 'left') : '', label, onclick: handleClick }));
877
+ return m(ButtonComponent, Object.assign(Object.assign({}, props), { style: buttonStyle, className: `${props.className || ''} ${cn}`, iconName: currentIconName, iconClass: label ? iconClass || 'left' : '', label, onclick: handleClick }));
814
878
  },
815
879
  };
816
880
  };
@@ -1277,11 +1341,19 @@
1277
1341
  };
1278
1342
  const rotation = (_a = rotationMap[direction]) !== null && _a !== void 0 ? _a : 0;
1279
1343
  const transform = rotation ? `rotate(${rotation}deg)` : undefined;
1344
+ const baseStyle = {
1345
+ display: 'inline-block',
1346
+ verticalAlign: 'middle',
1347
+ transform,
1348
+ };
1349
+ const combinedStyle = typeof style === 'string'
1350
+ ? `display:inline-block;vertical-align:middle;${transform ? `transform:${transform};` : ''}${style}`
1351
+ : Object.assign(Object.assign({}, baseStyle), style);
1280
1352
  const icon = iconPaths[name];
1281
1353
  if (!icon || !Array.isArray(icon)) {
1282
1354
  return m(Icon, Object.assign(Object.assign({}, props), { iconName: name }));
1283
1355
  }
1284
- return m('svg', Object.assign(Object.assign({}, props), { style: Object.assign({ display: 'inline-block', verticalAlign: 'middle', transform }, style), height: '24px', width: '24px', viewBox: '0 0 24 24', xmlns: 'http://www.w3.org/2000/svg' }), icon === null || icon === void 0 ? void 0 : icon.map((d) => m('path', {
1356
+ return m('svg', Object.assign(Object.assign({}, props), { style: combinedStyle, height: '24px', width: '24px', viewBox: '0 0 24 24', xmlns: 'http://www.w3.org/2000/svg' }), icon === null || icon === void 0 ? void 0 : icon.map((d) => m('path', {
1285
1357
  d,
1286
1358
  fill: d.includes('M0 0h24v24H0z') ? 'none' : 'currentColor',
1287
1359
  })));
@@ -1680,6 +1752,106 @@
1680
1752
  };
1681
1753
  };
1682
1754
 
1755
+ const isHandledKey = (key) => key === 'ArrowDown' || key === 'ArrowUp' || key === 'Enter' || key === ' ' || key === 'Escape';
1756
+ const getTotalRows = (optionCount, includeActionRow) => Math.max(0, optionCount) + (includeActionRow ? 1 : 0);
1757
+ const getComboboxKeyResult = ({ key, isOpen, focusedIndex, optionCount, includeActionRow, }) => {
1758
+ const totalRows = getTotalRows(optionCount, includeActionRow);
1759
+ if (!isHandledKey(key)) {
1760
+ return { isOpen, focusedIndex, action: 'none', preventDefault: false };
1761
+ }
1762
+ if (key === 'Escape') {
1763
+ return { isOpen: false, focusedIndex: -1, action: 'close', preventDefault: true };
1764
+ }
1765
+ if (key === 'ArrowDown') {
1766
+ if (!isOpen) {
1767
+ return {
1768
+ isOpen: true,
1769
+ focusedIndex: totalRows > 0 ? 0 : -1,
1770
+ action: 'open',
1771
+ preventDefault: true,
1772
+ };
1773
+ }
1774
+ return {
1775
+ isOpen: true,
1776
+ focusedIndex: totalRows > 0 ? Math.min(focusedIndex + 1, totalRows - 1) : -1,
1777
+ action: 'none',
1778
+ preventDefault: true,
1779
+ };
1780
+ }
1781
+ if (key === 'ArrowUp') {
1782
+ if (!isOpen) {
1783
+ return {
1784
+ isOpen: true,
1785
+ focusedIndex: totalRows > 0 ? 0 : -1,
1786
+ action: 'open',
1787
+ preventDefault: true,
1788
+ };
1789
+ }
1790
+ return {
1791
+ isOpen: true,
1792
+ focusedIndex: totalRows > 0 ? Math.max(focusedIndex - 1, 0) : -1,
1793
+ action: 'none',
1794
+ preventDefault: true,
1795
+ };
1796
+ }
1797
+ if (!isOpen) {
1798
+ return {
1799
+ isOpen: true,
1800
+ focusedIndex: totalRows > 0 ? 0 : -1,
1801
+ action: 'open',
1802
+ preventDefault: true,
1803
+ };
1804
+ }
1805
+ if (focusedIndex < 0) {
1806
+ return { isOpen, focusedIndex, action: 'none', preventDefault: true };
1807
+ }
1808
+ const isActionRow = includeActionRow && focusedIndex === optionCount;
1809
+ return {
1810
+ isOpen,
1811
+ focusedIndex,
1812
+ action: isActionRow ? 'selectAction' : 'selectFocused',
1813
+ preventDefault: true,
1814
+ };
1815
+ };
1816
+ const getComboboxOptionId = (baseId, optionIndex) => `${baseId}-option-${optionIndex}`;
1817
+ const createAsyncComboboxState = (initialOptions = []) => ({
1818
+ options: initialOptions,
1819
+ isLoading: false,
1820
+ error: null,
1821
+ latestRequestId: 0,
1822
+ });
1823
+ const startAsyncComboboxRequest = (state) => {
1824
+ const requestId = state.latestRequestId + 1;
1825
+ return {
1826
+ requestId,
1827
+ nextState: Object.assign(Object.assign({}, state), { isLoading: true, error: null, latestRequestId: requestId }),
1828
+ };
1829
+ };
1830
+ const resolveAsyncComboboxRequest = (state, requestId, options) => {
1831
+ if (requestId !== state.latestRequestId) {
1832
+ return state;
1833
+ }
1834
+ return Object.assign(Object.assign({}, state), { options, isLoading: false, error: null });
1835
+ };
1836
+ const rejectAsyncComboboxRequest = (state, requestId, errorMessage) => {
1837
+ if (requestId !== state.latestRequestId) {
1838
+ return state;
1839
+ }
1840
+ return Object.assign(Object.assign({}, state), { options: [], isLoading: false, error: errorMessage });
1841
+ };
1842
+ const getComboboxViewState = ({ isLoading, error, optionCount, }) => {
1843
+ if (isLoading) {
1844
+ return 'loading';
1845
+ }
1846
+ if (error) {
1847
+ return 'error';
1848
+ }
1849
+ if (optionCount === 0) {
1850
+ return 'empty';
1851
+ }
1852
+ return 'ready';
1853
+ };
1854
+
1683
1855
  exports.CollectionMode = void 0;
1684
1856
  (function (CollectionMode) {
1685
1857
  CollectionMode[CollectionMode["BASIC"] = 0] = "BASIC";
@@ -3289,6 +3461,10 @@
3289
3461
  },
3290
3462
  };
3291
3463
 
3464
+ const isReadonly = (attrs) => {
3465
+ const legacyReadonly = attrs.readonly;
3466
+ return Boolean(attrs.readOnly || legacyReadonly);
3467
+ };
3292
3468
  /** Character counter component that tracks text length against maxLength */
3293
3469
  const CharacterCounter = () => {
3294
3470
  return {
@@ -3367,7 +3543,7 @@
3367
3543
  return {
3368
3544
  oninit: ({ attrs }) => {
3369
3545
  const controlled = isControlled(attrs);
3370
- const isNonInteractive = attrs.readonly || attrs.disabled;
3546
+ const isNonInteractive = isReadonly(attrs) || attrs.disabled;
3371
3547
  // Warn developer for improper controlled usage
3372
3548
  if (attrs.value !== undefined && !controlled && !isNonInteractive) {
3373
3549
  console.warn(`TextArea received 'value' prop without 'oninput' or 'onchange' handler. ` +
@@ -3384,7 +3560,7 @@
3384
3560
  var _a, _b, _c, _d;
3385
3561
  const { className = 'col s12', helperText, iconName, id = state.id, value, placeholder, isMandatory, label, maxLength, oninput, onchange, onkeydown, onkeypress, onkeyup, onblur, style } = attrs, params = __rest(attrs, ["className", "helperText", "iconName", "id", "value", "placeholder", "isMandatory", "label", "maxLength", "oninput", "onchange", "onkeydown", "onkeypress", "onkeyup", "onblur", "style"]);
3386
3562
  const controlled = isControlled(attrs);
3387
- const isNonInteractive = attrs.readonly || attrs.disabled;
3563
+ const isNonInteractive = isReadonly(attrs) || attrs.disabled;
3388
3564
  let currentValue;
3389
3565
  if (controlled) {
3390
3566
  currentValue = value || '';
@@ -3500,14 +3676,12 @@
3500
3676
  onkeypress(ev, ev.target.value);
3501
3677
  }
3502
3678
  : undefined })),
3503
- m(Label, {
3679
+ ...renderFieldChrome({
3504
3680
  label,
3505
3681
  id,
3506
3682
  isMandatory,
3507
3683
  isActive: currentValue || placeholder || state.active,
3508
3684
  initialValue: currentValue !== '',
3509
- }),
3510
- m(HelperText, {
3511
3685
  helperText,
3512
3686
  dataError: state.hasInteracted && attrs.dataError ? attrs.dataError : undefined,
3513
3687
  dataSuccess: state.hasInteracted && attrs.dataSuccess ? attrs.dataSuccess : undefined,
@@ -3587,7 +3761,7 @@
3587
3761
  return {
3588
3762
  oninit: ({ attrs }) => {
3589
3763
  const controlled = isControlled(attrs);
3590
- const isNonInteractive = attrs.readonly || attrs.disabled;
3764
+ const isNonInteractive = isReadonly(attrs) || attrs.disabled;
3591
3765
  // Warn developer for improper controlled usage
3592
3766
  if (attrs.value !== undefined && !controlled && !isNonInteractive) {
3593
3767
  console.warn(`${type} input with label '${attrs.label}' received 'value' prop without 'oninput' handler. ` +
@@ -3627,7 +3801,7 @@
3627
3801
  }
3628
3802
  const isNumeric = ['number', 'range'].includes(type);
3629
3803
  const controlled = isControlled(attrs);
3630
- const isNonInteractive = attrs.readonly || attrs.disabled;
3804
+ const isNonInteractive = isReadonly(attrs) || attrs.disabled;
3631
3805
  let value;
3632
3806
  if (controlled) {
3633
3807
  value = attrs.value;
@@ -3646,9 +3820,28 @@
3646
3820
  const rangeType = type === 'range' && !attrs.minmax;
3647
3821
  // Only add validate class if input is interactive and validation is needed
3648
3822
  const shouldValidate = !isNonInteractive && (validate || type === 'email' || type === 'url' || isNumeric);
3823
+ const inputClass = [
3824
+ type === 'number' ? 'number-input' : '',
3825
+ type === 'range' && attrs.vertical ? 'range-slider vertical' : '',
3826
+ shouldValidate ? 'validate' : '',
3827
+ ]
3828
+ .filter(Boolean)
3829
+ .join(' ');
3830
+ const stepNumberInput = (direction) => {
3831
+ const input = state.inputElement;
3832
+ if (!input)
3833
+ return;
3834
+ if (direction === 'up') {
3835
+ input.stepUp();
3836
+ }
3837
+ else {
3838
+ input.stepDown();
3839
+ }
3840
+ input.dispatchEvent(new Event('input', { bubbles: true }));
3841
+ };
3649
3842
  return m('.input-field', { className: cn, style }, [
3650
3843
  iconName ? m('i.material-icons.prefix', iconName) : undefined,
3651
- m('input', Object.assign(Object.assign({ class: type === 'range' && attrs.vertical ? 'range-slider vertical' : shouldValidate ? 'validate' : undefined }, params), { type, tabindex: 0, id,
3844
+ m('input', Object.assign(Object.assign({ class: inputClass || undefined }, params), { type, tabindex: 0, id,
3652
3845
  placeholder, value: controlled ? value : undefined, style: type === 'range' && attrs.vertical
3653
3846
  ? {
3654
3847
  height: attrs.height || '200px',
@@ -3730,7 +3923,7 @@
3730
3923
  const target = e.target;
3731
3924
  state.hasInteracted = true;
3732
3925
  // Skip validation for readonly/disabled inputs
3733
- if (attrs.readonly || attrs.disabled) {
3926
+ if (isReadonly(attrs) || attrs.disabled) {
3734
3927
  // Call original onblur if provided
3735
3928
  if (attrs.onblur) {
3736
3929
  attrs.onblur(e);
@@ -3820,6 +4013,22 @@
3820
4013
  onchange(getValue(state.inputElement));
3821
4014
  }
3822
4015
  } })),
4016
+ type === 'number' && !isNonInteractive
4017
+ ? m('.number-input-controls', [
4018
+ m('button.number-input-control.number-input-control-up[type=button]', {
4019
+ 'aria-label': `Increase ${label || 'value'}`,
4020
+ title: 'Increase value',
4021
+ onmousedown: (event) => event.preventDefault(),
4022
+ onclick: () => stepNumberInput('up'),
4023
+ }, m(MaterialIcon, { name: 'chevron', direction: 'up' })),
4024
+ m('button.number-input-control.number-input-control-down[type=button]', {
4025
+ 'aria-label': `Decrease ${label || 'value'}`,
4026
+ title: 'Decrease value',
4027
+ onmousedown: (event) => event.preventDefault(),
4028
+ onclick: () => stepNumberInput('down'),
4029
+ }, m(MaterialIcon, { name: 'chevron' })),
4030
+ ])
4031
+ : undefined,
3823
4032
  // Clear button - only for text inputs with canClear enabled and has content
3824
4033
  canClear && type === 'text' && ((_f = state.inputElement) === null || _f === void 0 ? void 0 : _f.value)
3825
4034
  ? m(MaterialIcon, {
@@ -3832,14 +4041,12 @@
3832
4041
  },
3833
4042
  })
3834
4043
  : undefined,
3835
- m(Label, {
4044
+ ...renderFieldChrome({
3836
4045
  label,
3837
4046
  id,
3838
4047
  isMandatory,
3839
4048
  isActive,
3840
4049
  initialValue: value !== undefined && value !== '',
3841
- }),
3842
- m(HelperText, {
3843
4050
  helperText,
3844
4051
  dataError: state.hasInteracted && !state.isValid ? dataError : undefined,
3845
4052
  dataSuccess: state.hasInteracted && state.isValid ? dataSuccess : undefined,
@@ -3939,7 +4146,7 @@
3939
4146
  view: ({ attrs: { className = 'col s12', onchange, label, checked, disabled, description, style, inputId } }) => {
3940
4147
  if (!checkboxId)
3941
4148
  checkboxId = inputId || uniqueId();
3942
- return m('div', { className, style }, m('label', { for: checkboxId }, [
4149
+ return m('div', { className, style }, m('label', { for: checkboxId, style: { position: 'relative', display: 'inline-block' } }, [
3943
4150
  m('input[type=checkbox][tabindex=0]', {
3944
4151
  className: disabled ? 'disabled' : undefined,
3945
4152
  id: checkboxId,
@@ -4704,17 +4911,6 @@
4704
4911
  const updatePortalDropdown = (items, selectedLabel, onSelectItem, maxHeight) => {
4705
4912
  if (!state.isInsideModal)
4706
4913
  return;
4707
- // Clean up existing portal
4708
- const existingPortal = document.getElementById(`${state.id}-dropdown`);
4709
- if (existingPortal) {
4710
- existingPortal.remove();
4711
- }
4712
- if (!state.isOpen || !state.inputRef)
4713
- return;
4714
- // Create portal element
4715
- const portalElement = document.createElement('div');
4716
- portalElement.id = `${state.id}-dropdown`;
4717
- document.body.appendChild(portalElement);
4718
4914
  // Create dropdown content
4719
4915
  const availableItems = items.filter((item) => !item.divider && !item.disabled);
4720
4916
  const dropdownContent = items.map((item) => {
@@ -4728,15 +4924,14 @@
4728
4924
  class: `${isSelected ? 'selected' : ''} ${isFocused ? 'focused' : ''}${item.disabled ? ' disabled' : ''}`,
4729
4925
  onclick: item.disabled ? undefined : () => onSelectItem(item),
4730
4926
  }, m('span', {
4927
+ class: 'mm-layout-row mm-layout-row--center',
4731
4928
  style: {
4732
- display: 'flex',
4733
- alignItems: 'center',
4734
4929
  padding: '14px 16px',
4735
4930
  },
4736
4931
  }, [
4737
4932
  item.iconName
4738
4933
  ? m('i.material-icons', {
4739
- style: { marginRight: '32px' },
4934
+ class: 'mm-layout-item-icon',
4740
4935
  }, item.iconName)
4741
4936
  : undefined,
4742
4937
  item.label,
@@ -4753,8 +4948,12 @@
4753
4948
  state.dropdownRef = null;
4754
4949
  },
4755
4950
  }, dropdownContent);
4756
- // Render to portal
4757
- m.render(portalElement, dropdownVnode);
4951
+ syncPortalContent({
4952
+ containerId: `${state.id}-dropdown`,
4953
+ shouldRender: state.isOpen && !!state.inputRef,
4954
+ vnode: dropdownVnode,
4955
+ zIndex: 10000,
4956
+ });
4758
4957
  };
4759
4958
  return {
4760
4959
  oninit: ({ attrs }) => {
@@ -4775,15 +4974,19 @@
4775
4974
  // Cleanup global listener
4776
4975
  document.removeEventListener('click', closeDropdown);
4777
4976
  // Cleanup portal
4778
- const portalElement = document.getElementById(`${state.id}-dropdown`);
4779
- if (portalElement) {
4780
- portalElement.remove();
4781
- }
4977
+ syncPortalContent({ containerId: `${state.id}-dropdown`, shouldRender: false, vnode: null });
4782
4978
  },
4783
4979
  view: ({ attrs }) => {
4784
4980
  const { checkedId, key, label, onchange, disabled = false, items, iconName, helperText, style, className = 'col s12', } = attrs;
4785
4981
  const controlled = isControlled(attrs);
4786
- const currentCheckedId = controlled ? checkedId : state.internalCheckedId;
4982
+ const currentCheckedId = resolveControllableValue({
4983
+ controlled,
4984
+ disabled,
4985
+ controlledValue: checkedId,
4986
+ defaultValue: attrs.defaultCheckedId,
4987
+ internalValue: state.internalCheckedId,
4988
+ fallbackValue: undefined,
4989
+ });
4787
4990
  const handleSelection = (value) => {
4788
4991
  // Update internal state for uncontrolled mode
4789
4992
  if (!controlled) {
@@ -4815,6 +5018,7 @@
4815
5018
  iconName ? m('i.material-icons.prefix', iconName) : undefined,
4816
5019
  m(HelperText, { helperText }),
4817
5020
  m('.select-wrapper', {
5021
+ class: 'mm-layout-row mm-layout-row--center',
4818
5022
  onkeydown: disabled
4819
5023
  ? undefined
4820
5024
  : (e) => {
@@ -4882,15 +5086,14 @@
4882
5086
  handleSelection(value);
4883
5087
  },
4884
5088
  }, m('span', {
5089
+ class: 'mm-layout-row mm-layout-row--center',
4885
5090
  style: {
4886
- display: 'flex',
4887
- alignItems: 'center',
4888
5091
  padding: '14px 16px',
4889
5092
  },
4890
5093
  }, [
4891
5094
  item.iconName
4892
5095
  ? m('i.material-icons', {
4893
- style: { marginRight: '32px' },
5096
+ class: 'mm-layout-item-icon',
4894
5097
  }, item.iconName)
4895
5098
  : undefined,
4896
5099
  item.label,
@@ -6847,9 +7050,9 @@
6847
7050
  };
6848
7051
 
6849
7052
  const RadioButton = () => ({
6850
- view: ({ attrs: { id, groupId, label, onchange, className = 'col s12', checked, disabled, inputId } }) => {
7053
+ view: ({ attrs: { id, groupId, label, onchange, className = 'col s12', checked, disabled, inputId, allowHtml } }) => {
6851
7054
  const radioId = inputId || `${groupId}-${id}`;
6852
- return m('p', { className }, m('label', { for: radioId }, [
7055
+ return m('p', { className }, m('label', { for: radioId, style: { position: 'relative', display: 'inline-block' } }, [
6853
7056
  m('input[type=radio][tabindex=0]', {
6854
7057
  id: radioId,
6855
7058
  name: groupId,
@@ -6857,7 +7060,7 @@
6857
7060
  checked,
6858
7061
  onclick: onchange ? () => onchange(id) : undefined,
6859
7062
  }),
6860
- m('span', m.trust(label)),
7063
+ m('span', allowHtml ? m.trust(label) : label),
6861
7064
  ]));
6862
7065
  },
6863
7066
  });
@@ -6884,8 +7087,9 @@
6884
7087
  }
6885
7088
  },
6886
7089
  view: ({ attrs }) => {
6887
- var _a, _b;
6888
- const { checkedId, newRow, className = 'col s12', label = '', disabled, description, options, isMandatory, checkboxClass, layout = 'vertical', onchange, } = attrs;
7090
+ var _a, _b, _c;
7091
+ const { checkedId, newRow, className = 'col s12', label = '', disabled, description, options, isMandatory, checkboxClass, layout, direction, allowHtml = false, onchange, } = attrs;
7092
+ const resolvedLayout = (_a = layout !== null && layout !== void 0 ? layout : direction) !== null && _a !== void 0 ? _a : 'vertical';
6889
7093
  const { groupId, componentId } = state;
6890
7094
  const controlled = isControlled(attrs);
6891
7095
  // Get current checked ID from props or internal state
@@ -6895,11 +7099,11 @@
6895
7099
  }
6896
7100
  else if (disabled) {
6897
7101
  // Non-interactive components: prefer defaultCheckedId, fallback to checkedId
6898
- currentCheckedId = (_a = attrs.defaultCheckedId) !== null && _a !== void 0 ? _a : checkedId;
7102
+ currentCheckedId = (_b = attrs.defaultCheckedId) !== null && _b !== void 0 ? _b : checkedId;
6899
7103
  }
6900
7104
  else {
6901
7105
  // Interactive uncontrolled: use internal state
6902
- currentCheckedId = (_b = state.internalCheckedId) !== null && _b !== void 0 ? _b : attrs.defaultCheckedId;
7106
+ currentCheckedId = (_c = state.internalCheckedId) !== null && _c !== void 0 ? _c : attrs.defaultCheckedId;
6903
7107
  }
6904
7108
  const handleChange = (id) => {
6905
7109
  // Update internal state for uncontrolled mode
@@ -6914,16 +7118,16 @@
6914
7118
  const cn = [newRow ? 'clear' : '', className].filter(Boolean).join(' ').trim() || undefined;
6915
7119
  const radioItems = options.map((r) => ({
6916
7120
  component: (RadioButton),
6917
- props: Object.assign(Object.assign({}, r), { onchange: handleChange, groupId, disabled: disabled || r.disabled, className: checkboxClass, checked: r.id === currentCheckedId, inputId: `${componentId}-${r.id}` }),
7121
+ props: Object.assign(Object.assign({}, r), { onchange: handleChange, groupId, disabled: disabled || r.disabled, className: checkboxClass, checked: r.id === currentCheckedId, inputId: `${componentId}-${r.id}`, allowHtml }),
6918
7122
  key: r.id,
6919
7123
  }));
6920
7124
  const optionsContent = m(OptionsList, {
6921
7125
  options: radioItems,
6922
- layout,
7126
+ layout: resolvedLayout,
6923
7127
  });
6924
7128
  return m('div', { id: componentId, className: cn }, [
6925
7129
  label && m('h5.form-group-label', label + (isMandatory ? ' *' : '')),
6926
- description && m('p.helper-text', m.trust(description)),
7130
+ description && m('p.helper-text', allowHtml ? m.trust(description) : description),
6927
7131
  m('form', { action: '#' }, optionsContent),
6928
7132
  ]);
6929
7133
  },
@@ -7060,6 +7264,26 @@
7060
7264
  opacity: 1,
7061
7265
  };
7062
7266
  };
7267
+ const formatSelectionSummary = (attrs, selectedLabels, selectedIds, placeholder) => {
7268
+ const mode = attrs.summaryMode || 'labels';
7269
+ const selectedCount = selectedIds.length;
7270
+ const totalCount = attrs.options.filter((option) => !option.disabled).length;
7271
+ if (mode === 'labels') {
7272
+ return selectedLabels.length > 0 ? selectedLabels.join(', ') : placeholder;
7273
+ }
7274
+ const defaultCountLabel = `${selectedCount}/${totalCount} selected`;
7275
+ const formattedCount = attrs.countLabel ? attrs.countLabel(selectedCount, totalCount) : defaultCountLabel;
7276
+ if (mode === 'count') {
7277
+ return selectedCount > 0 ? formattedCount : placeholder;
7278
+ }
7279
+ if (selectedCount === 0) {
7280
+ return attrs.noneSelectedLabel || placeholder;
7281
+ }
7282
+ if (totalCount > 0 && selectedCount >= totalCount) {
7283
+ return attrs.allSelectedLabel || 'All selected';
7284
+ }
7285
+ return formattedCount;
7286
+ };
7063
7287
  const renderDropdownContent = (attrs, selectedIds, multiple, placeholder) => [
7064
7288
  placeholder && m('li.disabled', { tabindex: 0 }, m('span', placeholder)),
7065
7289
  // Render ungrouped options first
@@ -7128,17 +7352,6 @@
7128
7352
  const updatePortalDropdown = (attrs, selectedIds, multiple, placeholder) => {
7129
7353
  if (!state.isInsideModal)
7130
7354
  return;
7131
- // Clean up existing portal
7132
- const existingPortal = document.getElementById(state.dropdownId);
7133
- if (existingPortal) {
7134
- existingPortal.remove();
7135
- }
7136
- if (!state.isOpen || !state.inputRef)
7137
- return;
7138
- // Create portal element
7139
- const portalElement = document.createElement('div');
7140
- portalElement.id = state.dropdownId;
7141
- document.body.appendChild(portalElement);
7142
7355
  // Create dropdown with proper positioning
7143
7356
  const dropdownVnode = m('ul.dropdown-content.select-dropdown', {
7144
7357
  tabindex: 0,
@@ -7150,8 +7363,12 @@
7150
7363
  state.dropdownRef = null;
7151
7364
  },
7152
7365
  }, renderDropdownContent(attrs, selectedIds, multiple, placeholder));
7153
- // Render to portal
7154
- m.render(portalElement, dropdownVnode);
7366
+ syncPortalContent({
7367
+ containerId: state.dropdownId,
7368
+ shouldRender: state.isOpen && !!state.inputRef,
7369
+ vnode: dropdownVnode,
7370
+ zIndex: 10000,
7371
+ });
7155
7372
  };
7156
7373
  return {
7157
7374
  oninit: ({ attrs }) => {
@@ -7184,35 +7401,30 @@
7184
7401
  document.removeEventListener('click', closeDropdown);
7185
7402
  // Cleanup portaled dropdown if it exists
7186
7403
  if (state.isInsideModal && state.dropdownRef) {
7187
- const portalElement = document.getElementById(state.dropdownId);
7188
- if (portalElement && portalElement.parentNode) {
7189
- portalElement.parentNode.removeChild(portalElement);
7190
- }
7404
+ syncPortalContent({ containerId: state.dropdownId, shouldRender: false, vnode: null });
7191
7405
  }
7192
7406
  },
7193
7407
  view: ({ attrs }) => {
7194
- var _a;
7195
7408
  const controlled = isControlled(attrs);
7196
- const { newRow, className = 'col s12', key, options = [], multiple = false, label, helperText, placeholder = '', isMandatory, iconName, style, disabled, } = attrs;
7409
+ const { newRow, className = 'col s12', key, options = [], multiple = false, label, helperText, placeholder = '', isMandatory, iconName, appearance = 'standard', style, disabled, } = attrs;
7197
7410
  state.isMultiple = multiple;
7198
7411
  // Get selected IDs from props or internal state
7199
- let selectedIds;
7200
- if (controlled) {
7201
- selectedIds =
7202
- attrs.checkedId !== undefined ? (Array.isArray(attrs.checkedId) ? attrs.checkedId : [attrs.checkedId]) : [];
7203
- }
7204
- else if (disabled) {
7205
- // Non-interactive components: prefer defaultCheckedId, fallback to checkedId
7206
- const fallbackId = (_a = attrs.defaultCheckedId) !== null && _a !== void 0 ? _a : attrs.checkedId;
7207
- selectedIds = fallbackId !== undefined ? (Array.isArray(fallbackId) ? fallbackId : [fallbackId]) : [];
7208
- }
7209
- else {
7210
- // Interactive uncontrolled: use internal state
7211
- selectedIds = state.internalSelectedIds;
7212
- }
7213
- const finalClassName = newRow ? `${className} clear` : className;
7412
+ const selectedIds = resolveControllableValue({
7413
+ controlled,
7414
+ disabled,
7415
+ controlledValue: normalizeSelection(attrs.checkedId),
7416
+ defaultValue: normalizeSelection(attrs.defaultCheckedId),
7417
+ internalValue: state.internalSelectedIds,
7418
+ fallbackValue: [],
7419
+ });
7420
+ const layoutClassName = newRow ? `${className} clear` : className;
7421
+ const appearanceClassName = appearance === 'outlined' ? 'select-appearance-outlined' : '';
7422
+ const finalClassName = [layoutClassName, appearanceClassName].filter(Boolean).join(' ');
7423
+ const shouldInlineLabel = appearance === 'outlined' && !!label;
7214
7424
  const selectedOptionsUnsorted = options.filter((opt) => isSelected(opt.id, selectedIds));
7215
7425
  const selectedOptions = sortOptions(selectedOptionsUnsorted, attrs.sortSelected);
7426
+ const selectedLabels = selectedOptions.map((o) => o.label || o.id.toString());
7427
+ const triggerValue = formatSelectionSummary(attrs, selectedLabels, selectedIds, placeholder);
7216
7428
  // Update portal dropdown when inside modal
7217
7429
  if (state.isInsideModal) {
7218
7430
  updatePortalDropdown(attrs, selectedIds, multiple, placeholder);
@@ -7232,9 +7444,15 @@
7232
7444
  'aria-controls': state.dropdownId,
7233
7445
  role: 'combobox',
7234
7446
  }, [
7447
+ shouldInlineLabel &&
7448
+ m('span.select-inline-label', [
7449
+ label,
7450
+ isMandatory && m('span.mandatory', { style: { marginLeft: '2px' } }, '*'),
7451
+ ]),
7235
7452
  m('input[type=text][readonly=true].select-dropdown.dropdown-trigger', {
7236
7453
  id: state.id,
7237
- value: selectedOptions.length > 0 ? selectedOptions.map((o) => o.label || o.id).join(', ') : placeholder,
7454
+ value: triggerValue,
7455
+ 'aria-label': label,
7238
7456
  oncreate: ({ dom }) => {
7239
7457
  state.inputRef = dom;
7240
7458
  },
@@ -7266,14 +7484,12 @@
7266
7484
  }),
7267
7485
  ]),
7268
7486
  // Label
7269
- label &&
7270
- m(Label, {
7271
- id: state.id,
7272
- label,
7273
- isMandatory,
7274
- }),
7275
- // Helper text
7276
- helperText && m(HelperText, { helperText }),
7487
+ ...renderFieldChrome({
7488
+ label: shouldInlineLabel ? undefined : label,
7489
+ id: state.id,
7490
+ isMandatory,
7491
+ helperText,
7492
+ }),
7277
7493
  ]);
7278
7494
  },
7279
7495
  };
@@ -7492,59 +7708,65 @@
7492
7708
  };
7493
7709
  };
7494
7710
 
7495
- // Proper components to avoid anonymous closures
7496
- const SelectedChip = () => {
7497
- return {
7498
- view: ({ attrs: { option, onRemove } }) => m('.chip', [
7499
- option.label || option.id.toString(),
7500
- m(MaterialIcon, {
7501
- name: 'close',
7502
- className: 'close',
7503
- onclick: (e) => {
7504
- e.stopPropagation();
7505
- onRemove(option.id);
7506
- },
7507
- }),
7508
- ]),
7509
- };
7510
- };
7511
- const DropdownOption = () => {
7512
- return {
7513
- view: ({ attrs: { option, index, selectedIds, isFocused, onToggle, onMouseOver, showCheckbox } }) => {
7514
- const checkboxId = `search-select-option-${option.id}`;
7515
- const optionLabel = option.label || option.id.toString();
7516
- return m('li', {
7517
- key: option.id,
7518
- onclick: (e) => {
7519
- e.preventDefault();
7711
+ const SelectedChip = ({ option, onRemove, }) => m('.chip', [
7712
+ option.label || option.id.toString(),
7713
+ m(MaterialIcon, {
7714
+ name: 'close',
7715
+ className: 'close',
7716
+ onclick: (e) => {
7717
+ e.stopPropagation();
7718
+ onRemove(option.id);
7719
+ },
7720
+ }),
7721
+ ]);
7722
+ const DropdownOption = ({ option, index, optionId, selectedIds, isFocused, onToggle, onMouseOver, showCheckbox, }) => {
7723
+ const optionLabel = option.label || option.id.toString();
7724
+ return m('li', {
7725
+ id: optionId,
7726
+ role: 'option',
7727
+ 'aria-selected': selectedIds.includes(option.id) ? 'true' : 'false',
7728
+ class: `${option.disabled ? 'disabled' : ''} ${isFocused ? 'active' : ''}`.trim(),
7729
+ onmouseover: () => {
7730
+ if (!option.disabled) {
7731
+ onMouseOver(index);
7732
+ }
7733
+ },
7734
+ }, m('label', {
7735
+ class: 'search-select-option-label',
7736
+ onclick: (e) => {
7737
+ // A single-select row has no native checkbox to emit change.
7738
+ if (!showCheckbox) {
7739
+ e.preventDefault();
7740
+ onToggle(option);
7741
+ }
7742
+ },
7743
+ }, [
7744
+ showCheckbox &&
7745
+ m('input', {
7746
+ type: 'checkbox',
7747
+ checked: selectedIds.includes(option.id),
7748
+ disabled: option.disabled,
7749
+ onchange: (e) => {
7520
7750
  e.stopPropagation();
7521
7751
  onToggle(option);
7522
7752
  },
7523
- class: `${option.disabled ? 'disabled' : ''} ${isFocused ? 'active' : ''}`.trim(),
7524
- onmouseover: () => {
7525
- if (!option.disabled) {
7526
- onMouseOver(index);
7527
- }
7528
- },
7529
- }, m('label', { for: checkboxId, class: 'search-select-option-label' }, [
7530
- showCheckbox &&
7531
- m('input', {
7532
- type: 'checkbox',
7533
- id: checkboxId,
7534
- checked: selectedIds.includes(option.id),
7535
- }),
7536
- m('span', optionLabel),
7537
- ]));
7538
- },
7539
- };
7753
+ }),
7754
+ m('span', optionLabel),
7755
+ ]));
7540
7756
  };
7541
7757
  /**
7542
7758
  * Mithril Factory Component for Multi-Select Dropdown with search
7543
7759
  */
7544
- const SearchSelect = () => {
7760
+ const SearchSelect = (maybeVnode) => {
7761
+ var _a;
7762
+ const cachedInstance = (_a = maybeVnode === null || maybeVnode === void 0 ? void 0 : maybeVnode.state) === null || _a === void 0 ? void 0 : _a.__searchSelectInstance;
7763
+ if (cachedInstance) {
7764
+ return cachedInstance;
7765
+ }
7545
7766
  // State initialization
7546
7767
  const state = {
7547
7768
  id: '',
7769
+ listboxId: '',
7548
7770
  isOpen: false,
7549
7771
  searchTerm: '',
7550
7772
  inputRef: null,
@@ -7552,17 +7774,38 @@
7552
7774
  focusedIndex: -1,
7553
7775
  internalSelectedIds: [],
7554
7776
  createdOptions: [],
7555
- };
7777
+ asyncOptions: [],
7778
+ isLoading: false,
7779
+ loadError: null,
7780
+ latestRequestId: 0,
7781
+ };
7782
+ const updateAsyncState = (nextState) => {
7783
+ state.asyncOptions = nextState.options;
7784
+ state.isLoading = nextState.isLoading;
7785
+ state.loadError = nextState.error;
7786
+ state.latestRequestId = nextState.latestRequestId;
7787
+ };
7788
+ const readAsyncState = () => ({
7789
+ options: state.asyncOptions,
7790
+ isLoading: state.isLoading,
7791
+ error: state.loadError,
7792
+ latestRequestId: state.latestRequestId,
7793
+ });
7556
7794
  const isControlled = (attrs) => attrs.checkedId !== undefined && typeof attrs.onchange === 'function';
7557
7795
  const componentId = uniqueId();
7558
7796
  const searchInputId = `${componentId}-search`;
7559
7797
  // Handle click outside
7560
7798
  const handleClickOutside = (e) => {
7561
7799
  const target = e.target;
7800
+ const targetElement = e.target instanceof Element ? e.target : null;
7562
7801
  if (state.dropdownRef && state.dropdownRef.contains(target)) {
7563
7802
  // Click inside dropdown, do nothing
7564
7803
  return;
7565
7804
  }
7805
+ if (targetElement && targetElement.closest('.chips-container')) {
7806
+ // Click on trigger, do nothing
7807
+ return;
7808
+ }
7566
7809
  if (state.inputRef && state.inputRef.contains(target)) {
7567
7810
  // Click on trigger handled by onclick event
7568
7811
  return;
@@ -7573,40 +7816,51 @@
7573
7816
  }
7574
7817
  m.redraw();
7575
7818
  };
7576
- // Handle keyboard navigation
7577
- const handleKeyDown = (e, filteredOptions, showAddNew) => {
7578
- if (!state.isOpen)
7819
+ // Handle keyboard navigation through shared combobox primitive.
7820
+ const handleKeyDown = (e, optionCount, includeActionRow) => {
7821
+ const result = getComboboxKeyResult({
7822
+ key: e.key,
7823
+ isOpen: state.isOpen,
7824
+ focusedIndex: state.focusedIndex,
7825
+ optionCount,
7826
+ includeActionRow,
7827
+ });
7828
+ if (result.preventDefault) {
7829
+ e.preventDefault();
7830
+ }
7831
+ state.isOpen = result.isOpen;
7832
+ state.focusedIndex = result.focusedIndex;
7833
+ return result.action;
7834
+ };
7835
+ const loadAsyncOptions = async (attrs, query) => {
7836
+ if (!attrs.loadOptions) {
7579
7837
  return;
7580
- const totalOptions = filteredOptions.length + (showAddNew ? 1 : 0);
7581
- switch (e.key) {
7582
- case 'ArrowDown':
7583
- e.preventDefault();
7584
- state.focusedIndex = Math.min(state.focusedIndex + 1, totalOptions - 1);
7585
- break;
7586
- case 'ArrowUp':
7587
- e.preventDefault();
7588
- state.focusedIndex = Math.max(state.focusedIndex - 1, -1);
7589
- break;
7590
- case 'Enter':
7591
- e.preventDefault();
7592
- if (state.focusedIndex >= 0) {
7593
- if (showAddNew && state.focusedIndex === filteredOptions.length) {
7594
- // Handle add new option
7595
- return 'addNew';
7596
- }
7597
- else if (state.focusedIndex < filteredOptions.length) {
7598
- // This will be handled in the view method where attrs are available
7599
- return 'selectOption';
7600
- }
7601
- }
7602
- break;
7603
- case 'Escape':
7604
- e.preventDefault();
7605
- state.isOpen = false;
7838
+ }
7839
+ const started = startAsyncComboboxRequest(readAsyncState());
7840
+ updateAsyncState(started.nextState);
7841
+ m.redraw();
7842
+ try {
7843
+ const loadedOptions = await attrs.loadOptions(query);
7844
+ const resolved = resolveAsyncComboboxRequest(readAsyncState(), started.requestId, loadedOptions);
7845
+ updateAsyncState(resolved);
7846
+ if (started.requestId !== state.latestRequestId) {
7847
+ return;
7848
+ }
7849
+ if (state.focusedIndex >= loadedOptions.length) {
7606
7850
  state.focusedIndex = -1;
7607
- break;
7851
+ }
7852
+ }
7853
+ catch (error) {
7854
+ const rejected = rejectAsyncComboboxRequest(readAsyncState(), started.requestId, error instanceof Error ? error.message : 'Unable to load options');
7855
+ updateAsyncState(rejected);
7856
+ if (started.requestId !== state.latestRequestId) {
7857
+ return;
7858
+ }
7859
+ state.focusedIndex = -1;
7860
+ }
7861
+ finally {
7862
+ m.redraw();
7608
7863
  }
7609
- return null;
7610
7864
  };
7611
7865
  // Create new option and add to state
7612
7866
  const createAndSelectOption = async (attrs) => {
@@ -7687,9 +7941,10 @@
7687
7941
  attrs.onchange(newIds);
7688
7942
  }
7689
7943
  };
7690
- return {
7944
+ const componentInstance = {
7691
7945
  oninit: ({ attrs }) => {
7692
7946
  state.id = attrs.id || uniqueId();
7947
+ state.listboxId = `${state.id}-listbox`;
7693
7948
  // Initialize internal state for uncontrolled mode
7694
7949
  if (!isControlled(attrs)) {
7695
7950
  const defaultIds = attrs.defaultCheckedId !== undefined
@@ -7716,20 +7971,26 @@
7716
7971
  : [attrs.checkedId]
7717
7972
  : []
7718
7973
  : state.internalSelectedIds;
7719
- const { options = [], oncreateNewOption, className, placeholder, searchPlaceholder = 'Search options...', noOptionsFound = 'No options found', label, i18n = {}, maxDisplayedOptions, maxSelectedOptions, maxHeight, } = attrs;
7974
+ const { options = [], loadOptions, oncreateNewOption, className, placeholder, searchPlaceholder = 'Search options...', noOptionsFound = 'No options found', label, i18n = {}, maxDisplayedOptions, maxSelectedOptions, maxHeight, } = attrs;
7720
7975
  // Use i18n values if provided, otherwise use defaults
7721
7976
  const texts = {
7722
7977
  noOptionsFound: i18n.noOptionsFound || noOptionsFound,
7978
+ loadingOptions: i18n.loadingOptions || 'Loading options...',
7979
+ loadingError: i18n.loadingError || 'Unable to load options',
7723
7980
  addNewPrefix: i18n.addNewPrefix || '+',
7724
7981
  showingXofY: i18n.showingXofY || 'Showing {shown} of {total} options',
7725
7982
  maxSelectionsReached: i18n.maxSelectionsReached || 'Maximum {max} selections reached',
7726
7983
  };
7727
7984
  // Check if max selections is reached
7728
7985
  const isMaxSelectionsReached = maxSelectedOptions && selectedIds.length >= maxSelectedOptions;
7729
- // Merge provided options with internally created options
7730
- const allOptions = [...options, ...state.createdOptions];
7986
+ // In async mode, the active list is sourced remotely.
7987
+ const sourceOptions = loadOptions ? state.asyncOptions : options;
7988
+ // Merge active options with internally created options
7989
+ const allOptions = [...sourceOptions, ...state.createdOptions];
7990
+ // Keep selected label lookups stable across static, async, and created sets.
7991
+ const lookupOptions = [...options, ...state.asyncOptions, ...state.createdOptions];
7731
7992
  // Get selected options for display
7732
- const selectedOptionsUnsorted = allOptions.filter((opt) => selectedIds.includes(opt.id));
7993
+ const selectedOptionsUnsorted = lookupOptions.filter((opt) => selectedIds.includes(opt.id));
7733
7994
  const selectedOptions = sortOptions(selectedOptionsUnsorted, attrs.sortSelected);
7734
7995
  // Safely filter options
7735
7996
  const filteredOptions = allOptions.filter((option) => (option.label || option.id.toString()).toLowerCase().includes((state.searchTerm || '').toLowerCase()) &&
@@ -7742,6 +8003,14 @@
7742
8003
  const showAddNew = oncreateNewOption &&
7743
8004
  state.searchTerm &&
7744
8005
  !displayedOptions.some((o) => (o.label || o.id.toString()).toLowerCase() === state.searchTerm.toLowerCase());
8006
+ const activeDescendantId = state.isOpen && state.focusedIndex >= 0 && state.focusedIndex < displayedOptions.length
8007
+ ? getComboboxOptionId(state.id, state.focusedIndex)
8008
+ : undefined;
8009
+ const viewState = getComboboxViewState({
8010
+ isLoading: state.isLoading,
8011
+ error: state.loadError,
8012
+ optionCount: displayedOptions.length,
8013
+ });
7745
8014
  // Render the dropdown
7746
8015
  return m('.input-field.multi-select-dropdown', { className }, [
7747
8016
  m('.chips.chips-initial.chips-container', {
@@ -7752,14 +8021,34 @@
7752
8021
  // console.log('SearchSelect clicked', state.isOpen, e); // Debug log
7753
8022
  e.preventDefault();
7754
8023
  e.stopPropagation();
8024
+ const wasOpen = state.isOpen;
7755
8025
  state.isOpen = !state.isOpen;
8026
+ if (!wasOpen && state.isOpen && loadOptions) {
8027
+ void loadAsyncOptions(attrs, state.searchTerm);
8028
+ }
7756
8029
  // console.log('SearchSelect state changed to', state.isOpen); // Debug log
7757
8030
  },
7758
- class: 'chips chips-container',
8031
+ onkeydown: async (e) => {
8032
+ const action = handleKeyDown(e, displayedOptions.length, !!showAddNew);
8033
+ if (action === 'open' && loadOptions) {
8034
+ await loadAsyncOptions(attrs, state.searchTerm);
8035
+ return;
8036
+ }
8037
+ if (action === 'selectAction' && oncreateNewOption) {
8038
+ await createAndSelectOption(attrs);
8039
+ }
8040
+ if (action === 'selectFocused' && state.focusedIndex < displayedOptions.length) {
8041
+ toggleOption(displayedOptions[state.focusedIndex], attrs);
8042
+ }
8043
+ },
8044
+ class: 'chips chips-container mm-layout-row mm-layout-row--wrap mm-layout-row--align-end',
8045
+ role: 'combobox',
8046
+ tabindex: 0,
8047
+ 'aria-expanded': state.isOpen ? 'true' : 'false',
8048
+ 'aria-haspopup': 'listbox',
8049
+ 'aria-controls': state.isOpen ? state.listboxId : undefined,
8050
+ 'aria-activedescendant': activeDescendantId,
7759
8051
  style: {
7760
- display: 'flex',
7761
- alignItems: 'end',
7762
- flexWrap: 'wrap',
7763
8052
  cursor: 'pointer',
7764
8053
  position: 'relative',
7765
8054
  },
@@ -7772,30 +8061,41 @@
7772
8061
  value: selectedOptions.map((o) => o.label || o.id.toString()).join(', '),
7773
8062
  readonly: true,
7774
8063
  class: 'sr-only',
7775
- style: { position: 'absolute', left: '-9999px', opacity: 0 },
8064
+ style: {
8065
+ position: 'absolute',
8066
+ width: '1px',
8067
+ height: '1px',
8068
+ margin: '-1px',
8069
+ padding: 0,
8070
+ border: 0,
8071
+ overflow: 'hidden',
8072
+ clip: 'rect(0 0 0 0)',
8073
+ clipPath: 'inset(50%)',
8074
+ whiteSpace: 'nowrap',
8075
+ },
7776
8076
  }),
7777
8077
  // Selected Options (chips)
7778
- ...selectedOptions.map((option) => m(SelectedChip(), {
7779
- option,
8078
+ ...selectedOptions.map((option) => SelectedChip({
8079
+ option: option,
7780
8080
  onRemove: (id) => removeOption(id, attrs),
7781
8081
  })),
7782
8082
  // Placeholder when no options selected
7783
8083
  selectedOptions.length === 0 &&
7784
8084
  placeholder &&
7785
8085
  m('span.placeholder', {
8086
+ class: 'mm-layout-grow',
7786
8087
  style: {
7787
8088
  color: 'var(--mm-text-hint, #9e9e9e)',
7788
- flexGrow: 1,
7789
8089
  padding: '8px 0',
7790
8090
  },
7791
8091
  }, placeholder),
7792
8092
  // Spacer to push caret to the right
7793
- m('span.spacer', { style: { flexGrow: 1 } }),
8093
+ m('span.spacer.mm-layout-grow'),
7794
8094
  m(MaterialIcon, {
7795
8095
  name: 'caret',
7796
8096
  direction: state.isOpen ? 'up' : 'down',
7797
- class: 'caret',
7798
- style: { marginLeft: 'auto', cursor: 'pointer' },
8097
+ class: 'caret mm-layout-ml-auto',
8098
+ style: { cursor: 'pointer' },
7799
8099
  }),
7800
8100
  ]),
7801
8101
  // Label
@@ -7807,6 +8107,8 @@
7807
8107
  // Dropdown Menu
7808
8108
  state.isOpen &&
7809
8109
  m('ul.dropdown-content.select-dropdown', {
8110
+ id: state.listboxId,
8111
+ role: 'listbox',
7810
8112
  oncreate: ({ dom }) => {
7811
8113
  state.dropdownRef = dom;
7812
8114
  },
@@ -7831,23 +8133,39 @@
7831
8133
  oninput: (e) => {
7832
8134
  state.searchTerm = e.target.value;
7833
8135
  state.focusedIndex = -1; // Reset focus when typing
8136
+ if (loadOptions) {
8137
+ void loadAsyncOptions(attrs, state.searchTerm);
8138
+ }
7834
8139
  },
7835
8140
  onkeydown: async (e) => {
7836
- const result = handleKeyDown(e, displayedOptions, !!showAddNew);
7837
- if (result === 'addNew' && oncreateNewOption) {
8141
+ const action = handleKeyDown(e, displayedOptions.length, !!showAddNew);
8142
+ if (action === 'open' && loadOptions) {
8143
+ await loadAsyncOptions(attrs, state.searchTerm);
8144
+ }
8145
+ else if (action === 'selectAction' && oncreateNewOption) {
7838
8146
  await createAndSelectOption(attrs);
7839
8147
  }
7840
- else if (result === 'selectOption' && state.focusedIndex < displayedOptions.length) {
8148
+ else if (action === 'selectFocused' && state.focusedIndex < displayedOptions.length) {
7841
8149
  toggleOption(displayedOptions[state.focusedIndex], attrs);
7842
8150
  }
7843
8151
  },
7844
8152
  class: 'search-select-input',
8153
+ 'aria-autocomplete': 'list',
8154
+ 'aria-controls': state.listboxId,
7845
8155
  }),
7846
8156
  ]),
7847
- // No options found message or list of options
7848
- ...(displayedOptions.length === 0 && !showAddNew
7849
- ? [m('li.search-select-no-options', texts.noOptionsFound)]
8157
+ // Async loading status
8158
+ ...(viewState === 'loading'
8159
+ ? [m('li.search-select-loading-info', { role: 'status', 'aria-live': 'polite' }, texts.loadingOptions)]
8160
+ : []),
8161
+ // Async loading error
8162
+ ...(viewState === 'error' && state.loadError
8163
+ ? [
8164
+ m('li.search-select-error-info', { role: 'status', 'aria-live': 'assertive' }, `${texts.loadingError}: ${state.loadError}`),
8165
+ ]
7850
8166
  : []),
8167
+ // No options found message or list of options
8168
+ ...(viewState === 'empty' && !showAddNew ? [m('li.search-select-no-options', texts.noOptionsFound)] : []),
7851
8169
  // Truncation message
7852
8170
  ...(isTruncated
7853
8171
  ? [
@@ -7880,6 +8198,9 @@
7880
8198
  ...(showAddNew
7881
8199
  ? [
7882
8200
  m('li', {
8201
+ id: `${state.listboxId}-action`,
8202
+ role: 'option',
8203
+ 'aria-selected': 'false',
7883
8204
  onclick: async () => {
7884
8205
  await createAndSelectOption(attrs);
7885
8206
  },
@@ -7891,9 +8212,10 @@
7891
8212
  ]
7892
8213
  : []),
7893
8214
  // List of filtered options
7894
- ...displayedOptions.map((option, index) => m(DropdownOption(), {
8215
+ ...displayedOptions.map((option, index) => DropdownOption({
7895
8216
  option,
7896
8217
  index,
8218
+ optionId: getComboboxOptionId(state.id, index),
7897
8219
  selectedIds,
7898
8220
  isFocused: state.focusedIndex === index,
7899
8221
  onToggle: (opt) => toggleOption(opt, attrs),
@@ -7906,6 +8228,10 @@
7906
8228
  ]);
7907
8229
  },
7908
8230
  };
8231
+ if (maybeVnode === null || maybeVnode === void 0 ? void 0 : maybeVnode.state) {
8232
+ maybeVnode.state.__searchSelectInstance = componentInstance;
8233
+ }
8234
+ return componentInstance;
7909
8235
  };
7910
8236
 
7911
8237
  const defaultI18n$2 = {
@@ -9279,6 +9605,53 @@
9279
9605
  };
9280
9606
  };
9281
9607
 
9608
+ /** A semantic native fieldset for related controls. */
9609
+ const Fieldset = () => ({
9610
+ view: ({ attrs, children }) => {
9611
+ const { legend, description, required, disabled, error, className } = attrs, params = __rest(attrs, ["legend", "description", "required", "disabled", "error", "className"]);
9612
+ const descriptionId = params.id ? `${params.id}-description` : undefined;
9613
+ const errorId = params.id ? `${params.id}-error` : undefined;
9614
+ const describedBy = [description && descriptionId, error && errorId].filter(Boolean).join(' ') || undefined;
9615
+ return m('fieldset.mm-fieldset', Object.assign(Object.assign({}, params), { className, disabled, 'aria-describedby': describedBy }), [
9616
+ m('legend.mm-fieldset__legend', [legend, required && m('span.mm-fieldset__required[aria-hidden=true]', ' *')]),
9617
+ description && m('p.mm-fieldset__description', { id: descriptionId }, description),
9618
+ children,
9619
+ error && m('p.mm-fieldset__error[role=alert]', { id: errorId }, error),
9620
+ ]);
9621
+ },
9622
+ });
9623
+ const focusErrorTarget = (fieldId) => {
9624
+ var _a;
9625
+ if (!fieldId || typeof document === 'undefined')
9626
+ return;
9627
+ (_a = document.getElementById(fieldId)) === null || _a === void 0 ? void 0 : _a.focus();
9628
+ };
9629
+ /** A visual form section with an optional accessible validation summary. */
9630
+ const FormSection = () => ({
9631
+ view: ({ attrs, children }) => {
9632
+ const { title, description, errors = [], summaryTitle = 'Please correct the following errors', className } = attrs, params = __rest(attrs, ["title", "description", "errors", "summaryTitle", "className"]);
9633
+ return m('section.mm-form-section', Object.assign(Object.assign({}, params), { className }), [
9634
+ (title || description) &&
9635
+ m('.mm-form-section__header', [
9636
+ title && m('h3.mm-form-section__title', title),
9637
+ description && m('p.mm-form-section__description', description),
9638
+ ]),
9639
+ errors.length > 0 &&
9640
+ m('div.mm-validation-summary[role=alert][aria-live=assertive]', [
9641
+ m('p.mm-validation-summary__title', summaryTitle),
9642
+ m('ul.mm-validation-summary__list', errors.map((error, index) => {
9643
+ var _a, _b, _c;
9644
+ const href = (_a = error.href) !== null && _a !== void 0 ? _a : (error.fieldId ? `#${error.fieldId}` : undefined);
9645
+ return m('li', { key: `${(_c = (_b = error.fieldId) !== null && _b !== void 0 ? _b : href) !== null && _c !== void 0 ? _c : 'error'}-${index}` }, href
9646
+ ? m('a', { href, onclick: () => focusErrorTarget(error.fieldId) }, error.message)
9647
+ : error.message);
9648
+ })),
9649
+ ]),
9650
+ children,
9651
+ ]);
9652
+ },
9653
+ });
9654
+
9282
9655
  // List of MaterialIcon SVG icons that are available
9283
9656
  const materialIconSvgNames = [
9284
9657
  'caret',
@@ -9301,6 +9674,7 @@
9301
9674
  const renderIcon = (icon, style) => {
9302
9675
  if (!icon)
9303
9676
  return null;
9677
+ const objectStyle = typeof style === 'string' ? undefined : style;
9304
9678
  if (typeof icon === 'string') {
9305
9679
  // Check if this is a MaterialIcon SVG name
9306
9680
  if (materialIconSvgNames.includes(icon)) {
@@ -9317,7 +9691,7 @@
9317
9691
  // Image URL
9318
9692
  return m('img', {
9319
9693
  src: icon.content,
9320
- style: Object.assign(Object.assign({}, style), { width: '24px', height: '24px', objectFit: 'contain' }),
9694
+ style: Object.assign(Object.assign({}, objectStyle), { width: '24px', height: '24px', objectFit: 'contain' }),
9321
9695
  });
9322
9696
  }
9323
9697
  return null;
@@ -9328,8 +9702,10 @@
9328
9702
  const SidenavHeaderFooterItem = () => {
9329
9703
  return {
9330
9704
  view: ({ attrs }) => {
9331
- const { text, icon, onclick, href, className = '', _isExpanded = true, _position = 'left' } = attrs;
9705
+ const { text, icon, onclick, href, className = '', title, tooltip, tooltipWhenCollapsedOnly = true, _isExpanded = true, _position = 'left', } = attrs;
9332
9706
  const isRightAligned = _position === 'right';
9707
+ const tooltipText = title || tooltip || text;
9708
+ const shouldShowTooltip = tooltipWhenCollapsedOnly ? !_isExpanded : true;
9333
9709
  const handleClick = (e) => {
9334
9710
  if (onclick) {
9335
9711
  e.preventDefault();
@@ -9338,24 +9714,25 @@
9338
9714
  };
9339
9715
  const content = isRightAligned
9340
9716
  ? [
9341
- _isExpanded &&
9342
- m('span.sidenav-item-text', { style: { flex: '1', 'text-align': 'left', 'margin-right': '8px' } }, text),
9717
+ _isExpanded && m('span.sidenav-item-text.mm-layout-grow.mm-layout-mr-8.left-align', text),
9343
9718
  renderIcon(icon, { 'min-width': '24px', width: '24px' }),
9344
9719
  ]
9345
9720
  : [
9346
9721
  renderIcon(icon, { 'min-width': '24px', width: '24px' }),
9347
- _isExpanded && m('span.sidenav-item-text', { style: { 'margin-left': '8px', flex: '1' } }, text),
9722
+ _isExpanded && m('span.sidenav-item-text.mm-layout-grow.mm-layout-ml-8', text),
9348
9723
  ];
9349
9724
  const linkStyle = {
9350
- display: 'flex',
9351
- 'align-items': 'center',
9352
9725
  padding: _isExpanded ? '12px 16px' : '12px 18px',
9353
9726
  'justify-content': _isExpanded ? (isRightAligned ? 'flex-end' : 'flex-start') : 'center',
9354
9727
  };
9728
+ const linkClass = 'mm-layout-row mm-layout-row--center';
9355
9729
  return m('li', { class: className }, m('a', {
9356
9730
  href: href || '#!',
9357
9731
  onclick: handleClick,
9732
+ class: linkClass,
9358
9733
  style: linkStyle,
9734
+ title: shouldShowTooltip ? tooltipText : undefined,
9735
+ 'aria-label': shouldShowTooltip ? tooltipText : undefined,
9359
9736
  }, content));
9360
9737
  },
9361
9738
  };
@@ -9456,6 +9833,7 @@
9456
9833
  fixed ? 'sidenav-fixed' : '',
9457
9834
  mode === 'push' ? 'sidenav-push' : '',
9458
9835
  expandable && !isExpanded ? 'sidenav-collapsed' : '',
9836
+ 'mm-layout-stack',
9459
9837
  className,
9460
9838
  ]
9461
9839
  .filter(Boolean)
@@ -9465,8 +9843,6 @@
9465
9843
  transform: isOpen ? 'translateX(0)' : position === 'left' ? 'translateX(-105%)' : 'translateX(105%)',
9466
9844
  'transition-duration': `${animationDuration}ms`,
9467
9845
  'transition-property': 'transform, width',
9468
- display: 'flex',
9469
- 'flex-direction': 'column',
9470
9846
  },
9471
9847
  }, [
9472
9848
  // Header content slot (rendered first, before hamburger)
@@ -9483,10 +9859,8 @@
9483
9859
  // Hamburger toggle button (inside sidenav, at the top)
9484
9860
  showHamburger &&
9485
9861
  m('li.sidenav-hamburger-item', {
9862
+ class: `mm-layout-row mm-layout-row--center ${position === 'right' ? 'mm-layout-row--justify-end' : 'mm-layout-row--justify-start'}`,
9486
9863
  style: {
9487
- display: 'flex',
9488
- 'justify-content': position === 'right' ? 'flex-end' : 'flex-start',
9489
- 'align-items': 'center',
9490
9864
  padding: '12px 16px',
9491
9865
  cursor: 'pointer',
9492
9866
  'border-bottom': '1px solid rgba(0,0,0,0.1)',
@@ -9502,10 +9876,8 @@
9502
9876
  // Expand/collapse toggle button (if expandable, right below hamburger)
9503
9877
  expandable &&
9504
9878
  m('li.sidenav-expand-toggle', {
9879
+ class: `mm-layout-row mm-layout-row--center ${position === 'right' ? 'mm-layout-row--justify-end' : 'mm-layout-row--justify-start'}`,
9505
9880
  style: {
9506
- display: 'flex',
9507
- 'justify-content': position === 'right' ? 'flex-end' : 'flex-start',
9508
- 'align-items': 'center',
9509
9881
  padding: '12px 16px',
9510
9882
  cursor: 'pointer',
9511
9883
  'border-bottom': '1px solid rgba(0,0,0,0.1)',
@@ -9578,7 +9950,7 @@
9578
9950
  const submenuContent = isRightAligned
9579
9951
  ? [
9580
9952
  // Right-aligned: text on left, icons on right
9581
- isExpanded && m('span', { style: { flex: '1', 'text-align': 'left' } }, text),
9953
+ isExpanded && m('span.mm-layout-grow.left-align', text),
9582
9954
  icon && isExpanded && renderIcon(icon, { 'font-size': '18px' }),
9583
9955
  indicatorIcon,
9584
9956
  ]
@@ -9586,18 +9958,22 @@
9586
9958
  // Left-aligned: indicator on left, text and icon on right
9587
9959
  indicatorIcon,
9588
9960
  icon && isExpanded && renderIcon(icon, { 'font-size': '18px', 'margin-left': indicatorIcon ? '8px' : '0' }),
9589
- isExpanded && m('span', { style: { 'margin-left': icon || indicatorIcon ? '8px' : '0' } }, text),
9961
+ isExpanded && m('span', { class: icon || indicatorIcon ? 'mm-layout-ml-8' : undefined }, text),
9590
9962
  ];
9591
9963
  return m('li.sidenav-subitem', {
9592
- class: selected ? 'selected' : '',
9964
+ class: [
9965
+ selected ? 'selected' : '',
9966
+ 'mm-layout-row',
9967
+ 'mm-layout-row--center',
9968
+ 'mm-layout-gap-sm',
9969
+ isRightAligned ? 'mm-layout-row--justify-between' : 'mm-layout-row--justify-start',
9970
+ ]
9971
+ .filter(Boolean)
9972
+ .join(' ') || undefined,
9593
9973
  style: {
9594
9974
  padding: isExpanded ? '0 16px 0 48px' : '0 16px',
9595
9975
  cursor: 'pointer',
9596
- display: 'flex',
9597
- 'align-items': 'center',
9598
- gap: '8px',
9599
9976
  'font-size': '0.9em',
9600
- 'justify-content': isRightAligned ? 'space-between' : 'flex-start',
9601
9977
  height: '48px',
9602
9978
  'min-height': '48px',
9603
9979
  },
@@ -9614,7 +9990,7 @@
9614
9990
  let isSubmenuOpen = false;
9615
9991
  return {
9616
9992
  view: ({ attrs, children }) => {
9617
- const { text, icon, active = false, disabled = false, onclick, href, className = '', divider = false, subheader = false, submenu = [], submenuMode = 'checkbox', } = attrs;
9993
+ const { text, icon, active = false, disabled = false, onclick, href, className = '', divider = false, subheader = false, submenu = [], submenuMode = 'checkbox', title, tooltip, tooltipWhenCollapsedOnly = true, } = attrs;
9618
9994
  if (divider) {
9619
9995
  return m('li.divider');
9620
9996
  }
@@ -9639,39 +10015,45 @@
9639
10015
  const isExpanded = attrs._isExpanded !== false;
9640
10016
  const position = attrs._position || 'left';
9641
10017
  const isRightAligned = position === 'right';
10018
+ const tooltipText = title || tooltip || text;
10019
+ const shouldShowTooltip = tooltipWhenCollapsedOnly ? !isExpanded : true;
9642
10020
  // In expanded mode, icons are at the outside edge
9643
10021
  // In collapsed mode, icons are centered
9644
10022
  const content = isRightAligned
9645
10023
  ? [
9646
10024
  // Right-aligned: text on left, icon on right
9647
- isExpanded &&
9648
- m('span.sidenav-item-text', { style: { flex: '1', 'text-align': 'left', 'margin-right': '8px' } }, text || children),
10025
+ isExpanded && m('span.sidenav-item-text.mm-layout-grow.mm-layout-mr-8.left-align', text || children),
9649
10026
  renderIcon(icon, { 'min-width': '24px', width: '24px' }),
9650
10027
  ]
9651
10028
  : [
9652
10029
  // Left-aligned: icon on left, text on right
9653
10030
  renderIcon(icon, { 'min-width': '24px', width: '24px' }),
9654
- isExpanded && m('span.sidenav-item-text', { style: { 'margin-left': '8px', flex: '1' } }, text || children),
10031
+ isExpanded && m('span.sidenav-item-text.mm-layout-grow.mm-layout-ml-8', text || children),
9655
10032
  ];
9656
10033
  const linkStyle = {
9657
- display: 'flex',
9658
- 'align-items': 'center',
9659
10034
  padding: isExpanded ? '12px 16px' : '12px 18px',
9660
10035
  'justify-content': isExpanded ? (isRightAligned ? 'flex-end' : 'flex-start') : 'center',
9661
10036
  };
10037
+ const linkClass = 'mm-layout-row mm-layout-row--center';
9662
10038
  const mainItem = href && !disabled
9663
10039
  ? m('li', { class: itemClasses }, [
9664
10040
  m('a', {
9665
10041
  href,
9666
10042
  onclick: handleMainClick,
10043
+ class: linkClass,
9667
10044
  style: linkStyle,
10045
+ title: shouldShowTooltip ? tooltipText : undefined,
10046
+ 'aria-label': shouldShowTooltip ? tooltipText : undefined,
9668
10047
  }, content),
9669
10048
  ])
9670
10049
  : m('li', { class: itemClasses }, [
9671
10050
  m('a', {
9672
10051
  onclick: handleMainClick,
9673
10052
  href: '#!',
10053
+ class: linkClass,
9674
10054
  style: linkStyle,
10055
+ title: shouldShowTooltip ? tooltipText : undefined,
10056
+ 'aria-label': shouldShowTooltip ? tooltipText : undefined,
9675
10057
  }, content),
9676
10058
  ]);
9677
10059
  // Return main item with submenu if applicable
@@ -10187,8 +10569,9 @@
10187
10569
  const childIsFocused = ((_e = attrs.treeState) === null || _e === void 0 ? void 0 : _e.focusedNodeId) === child.id;
10188
10570
  // Calculate if this child is last in branch
10189
10571
  const childPath = [...(attrs.currentPath || []), childIndex];
10190
- const childIsLastInBranch = ((_f = attrs.treeAttrs) === null || _f === void 0 ? void 0 : _f.data) ?
10191
- isNodeLastInBranch(childPath, attrs.treeAttrs.data) : false;
10572
+ const childIsLastInBranch = ((_f = attrs.treeAttrs) === null || _f === void 0 ? void 0 : _f.data)
10573
+ ? isNodeLastInBranch(childPath, attrs.treeAttrs.data)
10574
+ : false;
10192
10575
  return m(TreeNodeComponent, {
10193
10576
  key: child.id,
10194
10577
  node: child,
@@ -10321,10 +10704,7 @@
10321
10704
  view: ({ attrs }) => {
10322
10705
  const { data, className, style, id, selectionMode = 'single', showConnectors = true } = attrs;
10323
10706
  return m('div.tree-view', {
10324
- class: [
10325
- className,
10326
- showConnectors && 'show-connectors'
10327
- ].filter(Boolean).join(' ') || undefined,
10707
+ class: [className, showConnectors && 'show-connectors'].filter(Boolean).join(' ') || undefined,
10328
10708
  style,
10329
10709
  id,
10330
10710
  role: selectionMode === 'multiple' ? 'listbox' : 'tree',
@@ -11022,6 +11402,7 @@
11022
11402
  };
11023
11403
  };
11024
11404
 
11405
+ const MOBILE_LAYOUT_BREAKPOINT = 600;
11025
11406
  /** Create a LikertScale component */
11026
11407
  const LikertScale = () => {
11027
11408
  const state = {
@@ -11084,6 +11465,22 @@
11084
11465
  return 'likert-scale--responsive';
11085
11466
  }
11086
11467
  };
11468
+ const isVerticalLayout = (layout) => {
11469
+ if (layout === 'vertical')
11470
+ return true;
11471
+ if (layout === 'horizontal')
11472
+ return false;
11473
+ return typeof window !== 'undefined' ? window.innerWidth <= MOBILE_LAYOUT_BREAKPOINT : false;
11474
+ };
11475
+ const getInlineAnchorLabel = (value, min, max, middleValue, startLabel, middleLabel, endLabel) => {
11476
+ if (value === min && startLabel)
11477
+ return startLabel;
11478
+ if (middleLabel && middleValue !== undefined && value === middleValue)
11479
+ return middleLabel;
11480
+ if (value === max && endLabel)
11481
+ return endLabel;
11482
+ return undefined;
11483
+ };
11087
11484
  const handleChange = (attrs, newValue) => {
11088
11485
  var _a;
11089
11486
  if (attrs.readonly || attrs.disabled)
@@ -11130,7 +11527,7 @@
11130
11527
  const LikertScaleItem = () => {
11131
11528
  return {
11132
11529
  view: ({ attrs }) => {
11133
- const { value, currentValue, showNumber, showTooltip, tooltipLabel, groupId, name, disabled, readonly, onchange, } = attrs;
11530
+ const { value, currentValue, showNumber, showTooltip, tooltipLabel, groupId, name, disabled, readonly, anchorLabel, onchange, } = attrs;
11134
11531
  const radioId = `${groupId}-${value}`;
11135
11532
  const isChecked = currentValue === value;
11136
11533
  return m('.likert-scale__item.no-select', {
@@ -11157,6 +11554,7 @@
11157
11554
  m('label.likert-scale__label', {
11158
11555
  for: radioId,
11159
11556
  }),
11557
+ anchorLabel && m('.likert-scale__item-anchor', anchorLabel),
11160
11558
  // Tooltip (optional)
11161
11559
  showTooltip && tooltipLabel && m('.likert-scale__tooltip', tooltipLabel),
11162
11560
  ]);
@@ -11180,6 +11578,8 @@
11180
11578
  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"]);
11181
11579
  const currentValue = getCurrentValue(attrs);
11182
11580
  const itemCount = Math.floor((max - min) / step) + 1;
11581
+ const useInlineAnchors = isVerticalLayout(layout);
11582
+ const middleValue = middleLabel ? min + Math.floor((itemCount - 1) / 2) * step : undefined;
11183
11583
  // Generate scale values
11184
11584
  const scaleValues = Array.from({ length: itemCount }, (_, i) => min + i * step);
11185
11585
  return m('.likert-scale', {
@@ -11228,6 +11628,9 @@
11228
11628
  showNumber: showNumbers,
11229
11629
  showTooltip: showTooltips,
11230
11630
  tooltipLabel: tooltipLabels === null || tooltipLabels === void 0 ? void 0 : tooltipLabels[value - min],
11631
+ anchorLabel: useInlineAnchors
11632
+ ? getInlineAnchorLabel(value, min, max, middleValue, startLabel, middleLabel, endLabel)
11633
+ : undefined,
11231
11634
  groupId: state.groupId,
11232
11635
  name,
11233
11636
  disabled,
@@ -11235,7 +11638,8 @@
11235
11638
  onchange: (v) => handleChange(attrs, v),
11236
11639
  }))),
11237
11640
  // Scale anchors
11238
- (startLabel || middleLabel || endLabel) &&
11641
+ !useInlineAnchors &&
11642
+ (startLabel || middleLabel || endLabel) &&
11239
11643
  m('.likert-scale__anchors', [
11240
11644
  startLabel && m('.likert-scale__anchor.likert-scale__anchor--start', startLabel),
11241
11645
  middleLabel && m('.likert-scale__anchor.likert-scale__anchor--middle', middleLabel),
@@ -11420,6 +11824,9 @@
11420
11824
  const { mode = 'indeterminate', value = 0, max = 100, size = 'medium', color = 'teal', colorIntensity, label, showPercentage = false, className = '', style = {}, id = state.id, 'aria-label': ariaLabel, 'aria-valuemin': ariaValueMin = 0, 'aria-valuemax': ariaValueMax = max, 'aria-valuenow': ariaValueNow, 'aria-valuetext': ariaValueText } = attrs, params = __rest(attrs, ["mode", "value", "max", "size", "color", "colorIntensity", "label", "showPercentage", "className", "style", "id", 'aria-label', 'aria-valuemin', 'aria-valuemax', 'aria-valuenow', 'aria-valuetext']);
11421
11825
  const isDeterminate = mode === 'determinate';
11422
11826
  const sizePixels = SIZE_MAP[size];
11827
+ const styleValue = typeof style === 'string'
11828
+ ? `width:${sizePixels}px;height:${sizePixels}px;${style}`
11829
+ : Object.assign({ width: `${sizePixels}px`, height: `${sizePixels}px` }, style);
11423
11830
  const { radius, circumference, strokeDashoffset, percentage } = isDeterminate
11424
11831
  ? calculateStrokeProperties(sizePixels, value, max)
11425
11832
  : { radius: 0, circumference: 0, strokeDashoffset: 0, percentage: 0 };
@@ -11446,7 +11853,7 @@
11446
11853
  : {
11447
11854
  'aria-valuetext': ariaValueText || label || 'Loading',
11448
11855
  };
11449
- return m('.circular-progress', Object.assign(Object.assign(Object.assign({}, params), { className: classNames, style: Object.assign({ width: `${sizePixels}px`, height: `${sizePixels}px` }, style), id, role: 'progressbar', 'aria-label': ariaLabel || (isDeterminate ? `Progress: ${Math.round(percentage)}%` : 'Loading') }), ariaAttrs), [
11856
+ return m('.circular-progress', Object.assign(Object.assign(Object.assign({}, params), { className: classNames, style: styleValue, id, role: 'progressbar', 'aria-label': ariaLabel || (isDeterminate ? `Progress: ${Math.round(percentage)}%` : 'Loading') }), ariaAttrs), [
11450
11857
  // SVG circle
11451
11858
  m('svg.circular-progress__svg', {
11452
11859
  viewBox: `0 0 ${sizePixels} ${sizePixels}`,
@@ -11511,13 +11918,7 @@
11511
11918
  // Determine label content
11512
11919
  const labelContent = label !== undefined ? label : showPercentage && isDeterminate ? `${Math.round(percentage)}%` : '';
11513
11920
  // Build class names
11514
- const classNames = [
11515
- 'linear-progress',
11516
- getColorClass(color, colorIntensity),
11517
- className,
11518
- ]
11519
- .filter(Boolean)
11520
- .join(' ');
11921
+ const classNames = ['linear-progress', getColorClass(color, colorIntensity), className].filter(Boolean).join(' ');
11521
11922
  // ARIA attributes
11522
11923
  const ariaAttrs = isDeterminate
11523
11924
  ? {
@@ -11594,10 +11995,12 @@
11594
11995
  exports.DoubleRangeSlider = DoubleRangeSlider;
11595
11996
  exports.Dropdown = Dropdown;
11596
11997
  exports.EmailInput = EmailInput;
11998
+ exports.Fieldset = Fieldset;
11597
11999
  exports.FileInput = FileInput;
11598
12000
  exports.FileUpload = FileUpload;
11599
12001
  exports.FlatButton = FlatButton;
11600
12002
  exports.FloatingActionButton = FloatingActionButton;
12003
+ exports.FormSection = FormSection;
11601
12004
  exports.HelperText = HelperText;
11602
12005
  exports.Icon = Icon;
11603
12006
  exports.IconButton = IconButton;
@@ -11649,6 +12052,7 @@
11649
12052
  exports.Timeline = Timeline;
11650
12053
  exports.Toast = Toast;
11651
12054
  exports.ToastComponent = ToastComponent;
12055
+ exports.ToggleButton = ToggleButton;
11652
12056
  exports.ToggleGroup = ToggleGroup;
11653
12057
  exports.Tooltip = Tooltip;
11654
12058
  exports.TooltipComponent = TooltipComponent;
@@ -11657,10 +12061,14 @@
11657
12061
  exports.Wizard = Wizard;
11658
12062
  exports.addLeadingZero = addLeadingZero;
11659
12063
  exports.clearPortal = clearPortal;
12064
+ exports.createAsyncComboboxState = createAsyncComboboxState;
11660
12065
  exports.createBreadcrumb = createBreadcrumb;
11661
12066
  exports.formatTime = formatTime;
11662
12067
  exports.generateHourOptions = generateHourOptions;
11663
12068
  exports.generateMinuteOptions = generateMinuteOptions;
12069
+ exports.getComboboxKeyResult = getComboboxKeyResult;
12070
+ exports.getComboboxOptionId = getComboboxOptionId;
12071
+ exports.getComboboxViewState = getComboboxViewState;
11664
12072
  exports.getDropdownStyles = getDropdownStyles;
11665
12073
  exports.getPortalContainer = getPortalContainer;
11666
12074
  exports.initPushpins = initPushpins;
@@ -11669,17 +12077,25 @@
11669
12077
  exports.isTimeDisabled = isTimeDisabled;
11670
12078
  exports.isValidationError = isValidationError;
11671
12079
  exports.isValidationSuccess = isValidationSuccess;
12080
+ exports.normalizeSelection = normalizeSelection;
11672
12081
  exports.padLeft = padLeft;
11673
12082
  exports.parseTime = parseTime;
11674
12083
  exports.range = range;
12084
+ exports.rejectAsyncComboboxRequest = rejectAsyncComboboxRequest;
11675
12085
  exports.releasePortalContainer = releasePortalContainer;
12086
+ exports.renderFieldChrome = renderFieldChrome;
11676
12087
  exports.renderToPortal = renderToPortal;
12088
+ exports.resolveAsyncComboboxRequest = resolveAsyncComboboxRequest;
12089
+ exports.resolveControllableValue = resolveControllableValue;
11677
12090
  exports.scrollToValue = scrollToValue;
11678
12091
  exports.snapToNearestItem = snapToNearestItem;
11679
12092
  exports.sortOptions = sortOptions;
12093
+ exports.startAsyncComboboxRequest = startAsyncComboboxRequest;
12094
+ exports.syncPortalContent = syncPortalContent;
11680
12095
  exports.timeToMinutes = timeToMinutes;
11681
12096
  exports.toast = toast;
11682
12097
  exports.uniqueId = uniqueId;
11683
12098
  exports.uuid4 = uuid4;
11684
12099
 
11685
12100
  }));
12101
+ //# sourceMappingURL=index.umd.js.map