mithril-materialized 2.0.0-beta.5 → 2.0.0-beta.7

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 (37) hide show
  1. package/dist/advanced.css +1888 -0
  2. package/dist/breadcrumb.d.ts +53 -0
  3. package/dist/components.css +2310 -0
  4. package/dist/core.css +3402 -0
  5. package/dist/file-upload.d.ts +34 -0
  6. package/dist/forms.css +2284 -0
  7. package/dist/index.css +1262 -83
  8. package/dist/index.d.ts +5 -0
  9. package/dist/index.esm.js +874 -33
  10. package/dist/index.js +885 -32
  11. package/dist/index.min.css +2 -2
  12. package/dist/index.umd.js +885 -32
  13. package/dist/pickers.css +487 -0
  14. package/dist/sidenav.d.ts +76 -0
  15. package/dist/tabs.d.ts +0 -2
  16. package/dist/theme-switcher.d.ts +49 -0
  17. package/dist/utilities.css +3197 -0
  18. package/dist/wizard.d.ts +58 -0
  19. package/package.json +11 -5
  20. package/sass/components/_breadcrumb.scss +248 -0
  21. package/sass/components/_buttons.scss +3 -3
  22. package/sass/components/_collapsible.scss +8 -8
  23. package/sass/components/_datepicker.scss +17 -15
  24. package/sass/components/_file-upload.scss +228 -0
  25. package/sass/components/_global.scss +7 -5
  26. package/sass/components/_modal.scss +5 -2
  27. package/sass/components/_navbar.scss +13 -5
  28. package/sass/components/_sidenav.scss +164 -7
  29. package/sass/components/_tabs.scss +5 -4
  30. package/sass/components/_theme-switcher.scss +120 -0
  31. package/sass/components/_theme-variables.scss +187 -0
  32. package/sass/components/_timepicker.scss +5 -5
  33. package/sass/components/_wizard.scss +416 -0
  34. package/sass/components/forms/_input-fields.scss +34 -12
  35. package/sass/components/forms/_radio-buttons.scss +10 -10
  36. package/sass/components/forms/_switches.scss +6 -6
  37. package/sass/materialize.scss +7 -0
package/dist/index.esm.js CHANGED
@@ -1158,23 +1158,8 @@ const CollapsibleItem = () => {
1158
1158
  header || iconName
1159
1159
  ? m('.collapsible-header', {
1160
1160
  onclick: onToggle,
1161
- style: {
1162
- cursor: 'pointer',
1163
- padding: '1rem',
1164
- backgroundColor: '#fff',
1165
- borderBottom: '1px solid #ddd',
1166
- display: 'flex',
1167
- alignItems: 'center',
1168
- transition: 'background-color 0.2s ease',
1169
- },
1170
- onmouseover: (e) => {
1171
- e.target.style.backgroundColor = '#f5f5f5';
1172
- },
1173
- onmouseleave: (e) => {
1174
- e.target.style.backgroundColor = '#fff';
1175
- },
1176
1161
  }, [
1177
- iconName ? m('i.material-icons', { style: { marginRight: '1rem' } }, iconName) : undefined,
1162
+ iconName ? m('i.material-icons', iconName) : undefined,
1178
1163
  header ? (typeof header === 'string' ? m('span', header) : header) : undefined,
1179
1164
  ])
1180
1165
  : undefined,
@@ -4394,13 +4379,9 @@ const Switch = () => {
4394
4379
  /** CSS-only Tabs component - no MaterializeCSS dependencies */
4395
4380
  const Tabs = () => {
4396
4381
  const toAnchored = () => {
4397
- let activeTabFound = false;
4398
4382
  return (tab) => {
4399
- const active = activeTabFound ? false : tab.active;
4400
- if (active)
4401
- activeTabFound = true;
4402
4383
  const tabId = createId(tab.title, tab.id);
4403
- return Object.assign(Object.assign({}, tab), { active, tabId, anchorId: `anchor-${tabId}` });
4384
+ return Object.assign(Object.assign({}, tab), { tabId, anchorId: `anchor-${tabId}` });
4404
4385
  };
4405
4386
  };
4406
4387
  const state = {
@@ -4411,7 +4392,9 @@ const Tabs = () => {
4411
4392
  indicatorStyle: {
4412
4393
  left: '0px',
4413
4394
  width: '0px',
4414
- }};
4395
+ },
4396
+ lastIndicatorUpdate: '',
4397
+ };
4415
4398
  const createId = (title, id) => (id ? id : title.replace(/ /g, '').toLowerCase());
4416
4399
  const updateIndicator = () => {
4417
4400
  const tabElement = document.getElementById(state.activeTabId);
@@ -4433,6 +4416,7 @@ const Tabs = () => {
4433
4416
  }
4434
4417
  };
4435
4418
  const handleTabClick = (tabId, tabElement, attrs) => {
4419
+ console.log({ state, tabId });
4436
4420
  if (state.activeTabId === tabId)
4437
4421
  return;
4438
4422
  state.activeTabId = tabId;
@@ -4492,20 +4476,15 @@ const Tabs = () => {
4492
4476
  const selectedTab = selectedTabId ? anchoredTabs.find((a) => a.tabId === selectedTabId) : undefined;
4493
4477
  if (selectedTab) {
4494
4478
  state.activeTabId = selectedTab.tabId;
4495
- selectedTab.active = true;
4496
4479
  return selectedTab;
4497
4480
  }
4498
- const activeTab = anchoredTabs.find((t) => t.active);
4499
- if (activeTab) {
4500
- // Active tab property takes precedence over selectedTabId
4501
- state.activeTabId = activeTab.tabId;
4502
- return activeTab;
4503
- }
4481
+ const curTab = state.activeTabId && anchoredTabs.find((a) => a.tabId === state.activeTabId);
4482
+ if (curTab)
4483
+ return curTab;
4504
4484
  // Default to first non-disabled tab
4505
- const firstAvailableTab = anchoredTabs.find((t) => !t.disabled && !t.href);
4485
+ const firstAvailableTab = anchoredTabs.find((a) => !a.disabled && !a.href);
4506
4486
  if (firstAvailableTab) {
4507
4487
  state.activeTabId = firstAvailableTab.tabId;
4508
- firstAvailableTab.active = true;
4509
4488
  return firstAvailableTab;
4510
4489
  }
4511
4490
  return undefined;
@@ -4544,7 +4523,7 @@ const Tabs = () => {
4544
4523
  disabled,
4545
4524
  }, m('a', {
4546
4525
  id: anchorId,
4547
- className: tab.active ? 'active' : undefined,
4526
+ className: tab.tabId === state.activeTabId ? 'active' : undefined,
4548
4527
  target,
4549
4528
  href: href || `#${anchorId}`,
4550
4529
  onclick: disabled || href
@@ -5339,4 +5318,866 @@ const initTooltips = (selector = '[data-tooltip]', options = {}) => {
5339
5318
  return tooltips;
5340
5319
  };
5341
5320
 
5342
- export { AnchorItem, Autocomplete, Button, ButtonFactory, Carousel, CharacterCounter, Chips, CodeBlock, Collapsible, CollapsibleItem, Collection, CollectionMode, ColorInput, DatePicker, Dropdown, EmailInput, FileInput, FlatButton, FloatingActionButton, HelperText, Icon, InputCheckbox, Label, LargeButton, ListItem, Mandatory, MaterialBox, ModalPanel, NumberInput, Options, Pagination, Parallax, PasswordInput, Pushpin, PushpinComponent, RadioButton, RadioButtons, RangeInput, RoundIconButton, SearchSelect, SecondaryContent, Select, SmallButton, SubmitButton, Switch, Tabs, TextArea, TextInput, TimePicker, Toast, ToastComponent, Tooltip, TooltipComponent, UrlInput, getDropdownStyles, initPushpins, initTooltips, isNumeric, padLeft, range, toast, uniqueId, uuid4 };
5321
+ /**
5322
+ * Theme switching utilities and component
5323
+ */
5324
+ class ThemeManager {
5325
+ /**
5326
+ * Set the theme for the entire application
5327
+ */
5328
+ static setTheme(theme) {
5329
+ this.currentTheme = theme;
5330
+ const root = document.documentElement;
5331
+ if (theme === 'auto') {
5332
+ // Remove explicit theme, let CSS media query handle it
5333
+ root.removeAttribute('data-theme');
5334
+ }
5335
+ else {
5336
+ // Set explicit theme
5337
+ root.setAttribute('data-theme', theme);
5338
+ }
5339
+ // Store preference in localStorage
5340
+ try {
5341
+ localStorage.setItem('mm-theme', theme);
5342
+ }
5343
+ catch (e) {
5344
+ // localStorage might not be available
5345
+ }
5346
+ }
5347
+ /**
5348
+ * Get the current theme
5349
+ */
5350
+ static getTheme() {
5351
+ return this.currentTheme;
5352
+ }
5353
+ /**
5354
+ * Get the effective theme (resolves 'auto' to actual theme)
5355
+ */
5356
+ static getEffectiveTheme() {
5357
+ if (this.currentTheme !== 'auto') {
5358
+ return this.currentTheme;
5359
+ }
5360
+ // Check CSS media query for auto mode
5361
+ if (typeof window !== 'undefined' && window.matchMedia) {
5362
+ return window.matchMedia('(prefers-color-scheme: dark)').matches ? 'dark' : 'light';
5363
+ }
5364
+ return 'light';
5365
+ }
5366
+ /**
5367
+ * Initialize theme from localStorage or system preference
5368
+ */
5369
+ static initialize() {
5370
+ let savedTheme = 'auto';
5371
+ try {
5372
+ const stored = localStorage.getItem('mm-theme');
5373
+ if (stored && ['light', 'dark', 'auto'].includes(stored)) {
5374
+ savedTheme = stored;
5375
+ }
5376
+ }
5377
+ catch (e) {
5378
+ // localStorage might not be available
5379
+ }
5380
+ this.setTheme(savedTheme);
5381
+ }
5382
+ /**
5383
+ * Toggle between light and dark themes
5384
+ */
5385
+ static toggle() {
5386
+ const current = this.getEffectiveTheme();
5387
+ this.setTheme(current === 'light' ? 'dark' : 'light');
5388
+ }
5389
+ }
5390
+ ThemeManager.currentTheme = 'auto';
5391
+ /**
5392
+ * Theme Switcher Component
5393
+ * Provides UI controls for changing themes
5394
+ */
5395
+ const ThemeSwitcher = () => {
5396
+ return {
5397
+ oninit: () => {
5398
+ // Initialize theme manager if not already done
5399
+ if (typeof window !== 'undefined') {
5400
+ ThemeManager.initialize();
5401
+ }
5402
+ },
5403
+ view: ({ attrs }) => {
5404
+ const { theme = ThemeManager.getTheme(), onThemeChange, showLabels = true, className = '' } = attrs;
5405
+ const handleThemeChange = (newTheme) => {
5406
+ ThemeManager.setTheme(newTheme);
5407
+ if (onThemeChange) {
5408
+ onThemeChange(newTheme);
5409
+ }
5410
+ };
5411
+ return m('.theme-switcher', { class: className }, [
5412
+ showLabels && m('span.theme-switcher-label', 'Theme:'),
5413
+ m('.theme-switcher-buttons', [
5414
+ m('button.btn-flat', {
5415
+ class: theme === 'light' ? 'active' : '',
5416
+ onclick: () => handleThemeChange('light'),
5417
+ title: 'Light theme'
5418
+ }, [
5419
+ m('i.material-icons', 'light_mode'),
5420
+ showLabels && m('span', 'Light')
5421
+ ]),
5422
+ m('button.btn-flat', {
5423
+ class: theme === 'auto' ? 'active' : '',
5424
+ onclick: () => handleThemeChange('auto'),
5425
+ title: 'Auto theme (system preference)'
5426
+ }, [
5427
+ m('i.material-icons', 'brightness_auto'),
5428
+ showLabels && m('span', 'Auto')
5429
+ ]),
5430
+ m('button.btn-flat', {
5431
+ class: theme === 'dark' ? 'active' : '',
5432
+ onclick: () => handleThemeChange('dark'),
5433
+ title: 'Dark theme'
5434
+ }, [
5435
+ m('i.material-icons', 'dark_mode'),
5436
+ showLabels && m('span', 'Dark')
5437
+ ])
5438
+ ])
5439
+ ]);
5440
+ }
5441
+ };
5442
+ };
5443
+ /**
5444
+ * Simple theme toggle button (just switches between light/dark)
5445
+ */
5446
+ const ThemeToggle = () => {
5447
+ return {
5448
+ oninit: () => {
5449
+ // Initialize theme manager if not already done
5450
+ if (typeof window !== 'undefined') {
5451
+ ThemeManager.initialize();
5452
+ }
5453
+ },
5454
+ view: ({ attrs }) => {
5455
+ const currentTheme = ThemeManager.getEffectiveTheme();
5456
+ return m('button.btn-flat.theme-toggle', {
5457
+ class: attrs.className || '',
5458
+ onclick: () => {
5459
+ ThemeManager.toggle();
5460
+ m.redraw();
5461
+ },
5462
+ title: `Switch to ${currentTheme === 'light' ? 'dark' : 'light'} theme`,
5463
+ style: 'margin: 0; height: 64px; line-height: 64px; border-radius: 0; min-width: 64px; padding: 0;'
5464
+ }, [
5465
+ m('i.material-icons', {
5466
+ style: 'color: inherit; font-size: 24px;'
5467
+ }, currentTheme === 'light' ? 'dark_mode' : 'light_mode')
5468
+ ]);
5469
+ }
5470
+ };
5471
+ };
5472
+
5473
+ /**
5474
+ * File Upload Component with Drag and Drop
5475
+ * Supports multiple files, file type validation, size limits, and image preview
5476
+ */
5477
+ const FileUpload = () => {
5478
+ let state;
5479
+ const validateFile = (file, attrs) => {
5480
+ // Check file size
5481
+ if (attrs.maxSize && file.size > attrs.maxSize) {
5482
+ const maxSizeMB = (attrs.maxSize / (1024 * 1024)).toFixed(1);
5483
+ return `File size exceeds ${maxSizeMB}MB limit`;
5484
+ }
5485
+ // Check file type
5486
+ if (attrs.accept) {
5487
+ const acceptedTypes = attrs.accept.split(',').map(type => type.trim());
5488
+ const isAccepted = acceptedTypes.some(acceptedType => {
5489
+ if (acceptedType.startsWith('.')) {
5490
+ // Extension check
5491
+ return file.name.toLowerCase().endsWith(acceptedType.toLowerCase());
5492
+ }
5493
+ else {
5494
+ // MIME type check
5495
+ return file.type.match(acceptedType.replace('*', '.*'));
5496
+ }
5497
+ });
5498
+ if (!isAccepted) {
5499
+ return `File type not accepted. Accepted: ${attrs.accept}`;
5500
+ }
5501
+ }
5502
+ return null;
5503
+ };
5504
+ const createFilePreview = (file) => {
5505
+ if (file.type.startsWith('image/')) {
5506
+ const reader = new FileReader();
5507
+ reader.onload = (e) => {
5508
+ var _a;
5509
+ file.preview = (_a = e.target) === null || _a === void 0 ? void 0 : _a.result;
5510
+ m.redraw();
5511
+ };
5512
+ reader.readAsDataURL(file);
5513
+ }
5514
+ };
5515
+ const handleFiles = (fileList, attrs) => {
5516
+ const newFiles = Array.from(fileList);
5517
+ const validFiles = [];
5518
+ // Validate each file
5519
+ for (const file of newFiles) {
5520
+ const error = validateFile(file, attrs);
5521
+ if (error) {
5522
+ file.uploadError = error;
5523
+ }
5524
+ else {
5525
+ validFiles.push(file);
5526
+ if (attrs.showPreview) {
5527
+ createFilePreview(file);
5528
+ }
5529
+ }
5530
+ }
5531
+ // Check max files limit
5532
+ if (attrs.maxFiles) {
5533
+ const totalFiles = state.files.length + validFiles.length;
5534
+ if (totalFiles > attrs.maxFiles) {
5535
+ const allowedCount = attrs.maxFiles - state.files.length;
5536
+ validFiles.splice(allowedCount);
5537
+ }
5538
+ }
5539
+ // Add valid files to state
5540
+ if (attrs.multiple) {
5541
+ state.files = [...state.files, ...validFiles];
5542
+ }
5543
+ else {
5544
+ state.files = validFiles.slice(0, 1);
5545
+ }
5546
+ // Notify parent component
5547
+ if (attrs.onFilesSelected) {
5548
+ attrs.onFilesSelected(state.files.filter(f => !f.uploadError));
5549
+ }
5550
+ };
5551
+ const removeFile = (fileToRemove, attrs) => {
5552
+ state.files = state.files.filter(file => file !== fileToRemove);
5553
+ if (attrs.onFileRemoved) {
5554
+ attrs.onFileRemoved(fileToRemove);
5555
+ }
5556
+ };
5557
+ const formatFileSize = (bytes) => {
5558
+ if (bytes === 0)
5559
+ return '0 B';
5560
+ const k = 1024;
5561
+ const sizes = ['B', 'KB', 'MB', 'GB'];
5562
+ const i = Math.floor(Math.log(bytes) / Math.log(k));
5563
+ return parseFloat((bytes / Math.pow(k, i)).toFixed(1)) + ' ' + sizes[i];
5564
+ };
5565
+ return {
5566
+ oninit: () => {
5567
+ state = {
5568
+ id: uniqueId(),
5569
+ files: [],
5570
+ isDragOver: false,
5571
+ isUploading: false
5572
+ };
5573
+ },
5574
+ view: ({ attrs }) => {
5575
+ const { accept, multiple = false, disabled = false, label = 'Choose files or drag them here', helperText, showPreview = true, className = '', error } = attrs;
5576
+ return m('.file-upload-container', { class: className }, [
5577
+ // Upload area
5578
+ m('.file-upload-area', {
5579
+ class: [
5580
+ state.isDragOver ? 'drag-over' : '',
5581
+ disabled ? 'disabled' : '',
5582
+ error ? 'error' : '',
5583
+ state.files.length > 0 ? 'has-files' : ''
5584
+ ].filter(Boolean).join(' '),
5585
+ ondragover: (e) => {
5586
+ if (disabled)
5587
+ return;
5588
+ e.preventDefault();
5589
+ e.stopPropagation();
5590
+ state.isDragOver = true;
5591
+ },
5592
+ ondragleave: (e) => {
5593
+ if (disabled)
5594
+ return;
5595
+ e.preventDefault();
5596
+ e.stopPropagation();
5597
+ state.isDragOver = false;
5598
+ },
5599
+ ondrop: (e) => {
5600
+ var _a;
5601
+ if (disabled)
5602
+ return;
5603
+ e.preventDefault();
5604
+ e.stopPropagation();
5605
+ state.isDragOver = false;
5606
+ if ((_a = e.dataTransfer) === null || _a === void 0 ? void 0 : _a.files) {
5607
+ handleFiles(e.dataTransfer.files, attrs);
5608
+ }
5609
+ },
5610
+ onclick: () => {
5611
+ if (disabled)
5612
+ return;
5613
+ const input = document.getElementById(state.id);
5614
+ input === null || input === void 0 ? void 0 : input.click();
5615
+ }
5616
+ }, [
5617
+ m('input[type="file"]', {
5618
+ id: state.id,
5619
+ accept,
5620
+ multiple,
5621
+ disabled,
5622
+ style: { display: 'none' },
5623
+ onchange: (e) => {
5624
+ const target = e.target;
5625
+ if (target.files) {
5626
+ handleFiles(target.files, attrs);
5627
+ }
5628
+ }
5629
+ }),
5630
+ m('.file-upload-content', [
5631
+ m('i.material-icons.file-upload-icon', 'cloud_upload'),
5632
+ m('p.file-upload-label', label),
5633
+ helperText && m('p.file-upload-helper', helperText),
5634
+ accept && m('p.file-upload-types', `Accepted: ${accept}`)
5635
+ ])
5636
+ ]),
5637
+ // Error message
5638
+ error && m('.file-upload-error', error),
5639
+ // File list
5640
+ state.files.length > 0 && m('.file-upload-list', [
5641
+ m('h6', 'Selected Files:'),
5642
+ state.files.map(file => m('.file-upload-item', { key: file.name + file.size }, [
5643
+ // Preview thumbnail
5644
+ showPreview && file.preview && m('.file-preview', [
5645
+ m('img', { src: file.preview, alt: file.name })
5646
+ ]),
5647
+ // File info
5648
+ m('.file-info', [
5649
+ m('.file-name', file.name),
5650
+ m('.file-details', [
5651
+ m('span.file-size', formatFileSize(file.size)),
5652
+ file.type && m('span.file-type', file.type)
5653
+ ]),
5654
+ // Progress bar (if uploading)
5655
+ file.uploadProgress !== undefined && m('.file-progress', [
5656
+ m('.progress', [
5657
+ m('.determinate', {
5658
+ style: { width: `${file.uploadProgress}%` }
5659
+ })
5660
+ ])
5661
+ ]),
5662
+ // Error message
5663
+ file.uploadError && m('.file-error', file.uploadError)
5664
+ ]),
5665
+ // Remove button
5666
+ m('button.btn-flat.file-remove', {
5667
+ onclick: (e) => {
5668
+ e.stopPropagation();
5669
+ removeFile(file, attrs);
5670
+ },
5671
+ title: 'Remove file'
5672
+ }, [
5673
+ m('i.material-icons', 'close')
5674
+ ])
5675
+ ]))
5676
+ ])
5677
+ ]);
5678
+ }
5679
+ };
5680
+ };
5681
+
5682
+ /**
5683
+ * Sidenav Component
5684
+ * A responsive navigation drawer that slides in from the side
5685
+ */
5686
+ const Sidenav = () => {
5687
+ let state;
5688
+ const handleBackdropClick = (attrs) => {
5689
+ if (attrs.closeOnBackdropClick !== false && attrs.onToggle) {
5690
+ attrs.onToggle(false);
5691
+ }
5692
+ };
5693
+ const handleEscapeKey = (e, attrs) => {
5694
+ if (e.key === 'Escape' && attrs.closeOnEscape !== false && attrs.onToggle) {
5695
+ attrs.onToggle(false);
5696
+ }
5697
+ };
5698
+ const setBodyOverflow = (isOpen, mode) => {
5699
+ if (typeof document !== 'undefined') {
5700
+ document.body.style.overflow = isOpen && mode === 'overlay' ? 'hidden' : '';
5701
+ }
5702
+ };
5703
+ return {
5704
+ oninit: ({ attrs }) => {
5705
+ state = {
5706
+ id: attrs.id || uniqueId(),
5707
+ isOpen: attrs.isOpen || false,
5708
+ isAnimating: false
5709
+ };
5710
+ // Set up keyboard listener
5711
+ if (typeof document !== 'undefined' && attrs.closeOnEscape !== false) {
5712
+ document.addEventListener('keydown', (e) => handleEscapeKey(e, attrs));
5713
+ }
5714
+ },
5715
+ onbeforeupdate: ({ attrs }) => {
5716
+ const wasOpen = state.isOpen;
5717
+ const isOpen = attrs.isOpen || false;
5718
+ if (wasOpen !== isOpen) {
5719
+ state.isOpen = isOpen;
5720
+ state.isAnimating = true;
5721
+ setBodyOverflow(isOpen, attrs.mode || 'overlay');
5722
+ // Clear animation state after animation completes
5723
+ setTimeout(() => {
5724
+ state.isAnimating = false;
5725
+ m.redraw();
5726
+ }, attrs.animationDuration || 300);
5727
+ }
5728
+ },
5729
+ onremove: ({ attrs }) => {
5730
+ // Clean up
5731
+ setBodyOverflow(false, attrs.mode || 'overlay');
5732
+ if (typeof document !== 'undefined' && attrs.closeOnEscape !== false) {
5733
+ document.removeEventListener('keydown', (e) => handleEscapeKey(e, attrs));
5734
+ }
5735
+ },
5736
+ view: ({ attrs, children }) => {
5737
+ const { position = 'left', mode = 'overlay', width = 300, className = '', showBackdrop = true, animationDuration = 300, fixed = false } = attrs;
5738
+ const isOpen = state.isOpen;
5739
+ return [
5740
+ // Backdrop (using existing materialize class)
5741
+ showBackdrop && mode === 'overlay' && m('.sidenav-overlay', {
5742
+ style: {
5743
+ display: isOpen ? 'block' : 'none',
5744
+ opacity: isOpen ? '1' : '0'
5745
+ },
5746
+ onclick: () => handleBackdropClick(attrs)
5747
+ }),
5748
+ // Sidenav (using existing materialize structure)
5749
+ m('ul.sidenav', {
5750
+ id: state.id,
5751
+ class: [
5752
+ position === 'right' ? 'right-aligned' : '',
5753
+ fixed ? 'sidenav-fixed' : '',
5754
+ className
5755
+ ].filter(Boolean).join(' '),
5756
+ style: {
5757
+ width: `${width}px`,
5758
+ transform: isOpen ? 'translateX(0)' :
5759
+ position === 'left' ? 'translateX(-105%)' : 'translateX(105%)',
5760
+ 'transition-duration': `${animationDuration}ms`
5761
+ }
5762
+ }, children)
5763
+ ];
5764
+ }
5765
+ };
5766
+ };
5767
+ /**
5768
+ * Sidenav Item Component
5769
+ * Individual items for the sidenav menu
5770
+ */
5771
+ const SidenavItem = () => {
5772
+ return {
5773
+ view: ({ attrs, children }) => {
5774
+ const { text, icon, active = false, disabled = false, onclick, href, className = '', divider = false, subheader = false } = attrs;
5775
+ if (divider) {
5776
+ return m('li.divider');
5777
+ }
5778
+ if (subheader) {
5779
+ return m('li.subheader', text || children);
5780
+ }
5781
+ const itemClasses = [
5782
+ active ? 'active' : '',
5783
+ disabled ? 'disabled' : '',
5784
+ className
5785
+ ].filter(Boolean).join(' ');
5786
+ const content = [
5787
+ icon && m('i.material-icons', icon),
5788
+ text || children
5789
+ ];
5790
+ if (href && !disabled) {
5791
+ return m('li', { class: itemClasses }, [
5792
+ m('a', {
5793
+ href,
5794
+ onclick: disabled ? undefined : onclick
5795
+ }, content)
5796
+ ]);
5797
+ }
5798
+ return m('li', { class: itemClasses }, [
5799
+ m('a', {
5800
+ onclick: disabled ? undefined : onclick,
5801
+ href: '#!'
5802
+ }, content)
5803
+ ]);
5804
+ }
5805
+ };
5806
+ };
5807
+ /**
5808
+ * Sidenav utilities for programmatic control
5809
+ */
5810
+ class SidenavManager {
5811
+ /**
5812
+ * Open a sidenav by ID
5813
+ */
5814
+ static open(id) {
5815
+ const element = document.getElementById(id);
5816
+ if (element) {
5817
+ element.classList.add('open');
5818
+ element.classList.remove('closed');
5819
+ }
5820
+ }
5821
+ /**
5822
+ * Close a sidenav by ID
5823
+ */
5824
+ static close(id) {
5825
+ const element = document.getElementById(id);
5826
+ if (element) {
5827
+ element.classList.remove('open');
5828
+ element.classList.add('closed');
5829
+ }
5830
+ }
5831
+ /**
5832
+ * Toggle a sidenav by ID
5833
+ */
5834
+ static toggle(id) {
5835
+ const element = document.getElementById(id);
5836
+ if (element) {
5837
+ const isOpen = element.classList.contains('open');
5838
+ if (isOpen) {
5839
+ this.close(id);
5840
+ }
5841
+ else {
5842
+ this.open(id);
5843
+ }
5844
+ }
5845
+ }
5846
+ }
5847
+
5848
+ /**
5849
+ * Breadcrumb Component
5850
+ * Displays a navigation path showing the user's location within a site hierarchy
5851
+ */
5852
+ const Breadcrumb = () => {
5853
+ return {
5854
+ view: ({ attrs }) => {
5855
+ const { items = [], separator = 'chevron_right', className = '', showIcons = false, maxItems, showHome = false } = attrs;
5856
+ if (items.length === 0) {
5857
+ return null;
5858
+ }
5859
+ let displayItems = [...items];
5860
+ // Handle max items with ellipsis
5861
+ if (maxItems && items.length > maxItems) {
5862
+ const firstItem = items[0];
5863
+ const lastItems = items.slice(-(maxItems - 2));
5864
+ displayItems = [
5865
+ firstItem,
5866
+ { text: '...', disabled: true, className: 'breadcrumb-ellipsis' },
5867
+ ...lastItems
5868
+ ];
5869
+ }
5870
+ return m('nav.breadcrumb', { class: className }, [
5871
+ m('ol.breadcrumb-list', displayItems.map((item, index) => {
5872
+ const isLast = index === displayItems.length - 1;
5873
+ const isFirst = index === 0;
5874
+ return [
5875
+ // Breadcrumb item
5876
+ m('li.breadcrumb-item', {
5877
+ class: [
5878
+ item.active || isLast ? 'active' : '',
5879
+ item.disabled ? 'disabled' : '',
5880
+ item.className || ''
5881
+ ].filter(Boolean).join(' ')
5882
+ }, [
5883
+ item.href && !item.disabled && !isLast ?
5884
+ // Link item
5885
+ m('a.breadcrumb-link', {
5886
+ href: item.href,
5887
+ onclick: item.onclick
5888
+ }, [
5889
+ (showIcons && item.icon) && m('i.material-icons.breadcrumb-icon', item.icon),
5890
+ (showHome && isFirst && !item.icon) && m('i.material-icons.breadcrumb-icon', 'home'),
5891
+ m('span.breadcrumb-text', item.text)
5892
+ ]) :
5893
+ // Text item (active or disabled)
5894
+ m('span.breadcrumb-text', {
5895
+ onclick: item.disabled ? undefined : item.onclick
5896
+ }, [
5897
+ (showIcons && item.icon) && m('i.material-icons.breadcrumb-icon', item.icon),
5898
+ (showHome && isFirst && !item.icon) && m('i.material-icons.breadcrumb-icon', 'home'),
5899
+ item.text
5900
+ ])
5901
+ ]),
5902
+ // Separator (except for last item)
5903
+ !isLast && m('li.breadcrumb-separator', [
5904
+ m('i.material-icons', separator)
5905
+ ])
5906
+ ];
5907
+ }).reduce((acc, val) => acc.concat(val), []))
5908
+ ]);
5909
+ }
5910
+ };
5911
+ };
5912
+ /**
5913
+ * Simple Breadcrumb utility for common use cases
5914
+ */
5915
+ const createBreadcrumb = (path, basePath = '/') => {
5916
+ const segments = path.split('/').filter(Boolean);
5917
+ const items = [];
5918
+ // Add home item
5919
+ items.push({
5920
+ text: 'Home',
5921
+ href: basePath,
5922
+ icon: 'home'
5923
+ });
5924
+ // Add path segments
5925
+ let currentPath = basePath;
5926
+ segments.forEach((segment, index) => {
5927
+ currentPath += (currentPath.endsWith('/') ? '' : '/') + segment;
5928
+ const isLast = index === segments.length - 1;
5929
+ items.push({
5930
+ text: segment.charAt(0).toUpperCase() + segment.slice(1).replace(/-/g, ' '),
5931
+ href: isLast ? undefined : currentPath,
5932
+ active: isLast
5933
+ });
5934
+ });
5935
+ return items;
5936
+ };
5937
+ /**
5938
+ * Breadcrumb utilities
5939
+ */
5940
+ class BreadcrumbManager {
5941
+ /**
5942
+ * Create breadcrumb items from a route path
5943
+ */
5944
+ static fromRoute(route, routeConfig = {}) {
5945
+ const segments = route.split('/').filter(Boolean);
5946
+ const items = [];
5947
+ // Add home
5948
+ items.push({
5949
+ text: 'Home',
5950
+ href: '/',
5951
+ icon: 'home'
5952
+ });
5953
+ let currentPath = '';
5954
+ segments.forEach((segment, index) => {
5955
+ currentPath += '/' + segment;
5956
+ const isLast = index === segments.length - 1;
5957
+ // Use custom text from config or format segment
5958
+ const text = routeConfig[currentPath] ||
5959
+ segment.charAt(0).toUpperCase() + segment.slice(1).replace(/-/g, ' ');
5960
+ items.push({
5961
+ text,
5962
+ href: isLast ? undefined : currentPath,
5963
+ active: isLast
5964
+ });
5965
+ });
5966
+ return items;
5967
+ }
5968
+ /**
5969
+ * Create breadcrumb items from a hierarchical object
5970
+ */
5971
+ static fromHierarchy(hierarchy, textKey = 'name', pathKey = 'path') {
5972
+ return hierarchy.map((item, index) => ({
5973
+ text: item[textKey],
5974
+ href: index === hierarchy.length - 1 ? undefined : item[pathKey],
5975
+ active: index === hierarchy.length - 1
5976
+ }));
5977
+ }
5978
+ }
5979
+
5980
+ /**
5981
+ * Wizard/Stepper Component
5982
+ * A multi-step interface for guiding users through a process
5983
+ */
5984
+ const Wizard = () => {
5985
+ let state;
5986
+ const validateStep = async (stepIndex, steps) => {
5987
+ const step = steps[stepIndex];
5988
+ if (!step || !step.validate)
5989
+ return true;
5990
+ state.isValidating = true;
5991
+ try {
5992
+ const isValid = await step.validate();
5993
+ if (isValid) {
5994
+ state.completedSteps.add(stepIndex);
5995
+ state.errorSteps.delete(stepIndex);
5996
+ }
5997
+ else {
5998
+ state.errorSteps.add(stepIndex);
5999
+ state.completedSteps.delete(stepIndex);
6000
+ }
6001
+ return isValid;
6002
+ }
6003
+ catch (error) {
6004
+ state.errorSteps.add(stepIndex);
6005
+ state.completedSteps.delete(stepIndex);
6006
+ return false;
6007
+ }
6008
+ finally {
6009
+ state.isValidating = false;
6010
+ m.redraw();
6011
+ }
6012
+ };
6013
+ const goToStep = async (stepIndex, attrs) => {
6014
+ const { linear = true, onStepChange, steps } = attrs;
6015
+ if (stepIndex < 0 || stepIndex >= steps.length)
6016
+ return false;
6017
+ // Check if step is disabled
6018
+ if (steps[stepIndex].disabled)
6019
+ return false;
6020
+ // In linear mode, validate all previous steps
6021
+ if (linear && stepIndex > state.currentStep) {
6022
+ for (let i = state.currentStep; i < stepIndex; i++) {
6023
+ const isValid = await validateStep(i, steps);
6024
+ if (!isValid && !steps[i].optional) {
6025
+ return false;
6026
+ }
6027
+ }
6028
+ }
6029
+ // Validate current step before moving forward
6030
+ if (stepIndex > state.currentStep) {
6031
+ const isValid = await validateStep(state.currentStep, steps);
6032
+ if (!isValid && !steps[state.currentStep].optional) {
6033
+ return false;
6034
+ }
6035
+ }
6036
+ const oldStep = state.currentStep;
6037
+ state.currentStep = stepIndex;
6038
+ // Always call onStepChange when step changes
6039
+ if (onStepChange && oldStep !== stepIndex) {
6040
+ onStepChange(stepIndex, steps[stepIndex].id || `step-${stepIndex}`);
6041
+ }
6042
+ // Force redraw to update UI
6043
+ m.redraw();
6044
+ return true;
6045
+ };
6046
+ const nextStep = async (attrs) => {
6047
+ const { steps } = attrs;
6048
+ // Check if we're on the last step
6049
+ if (state.currentStep === steps.length - 1) {
6050
+ // This is the complete action
6051
+ if (attrs.onComplete) {
6052
+ attrs.onComplete();
6053
+ }
6054
+ return;
6055
+ }
6056
+ // Try to move to next step
6057
+ await goToStep(state.currentStep + 1, attrs);
6058
+ };
6059
+ const previousStep = (attrs) => {
6060
+ goToStep(state.currentStep - 1, attrs);
6061
+ };
6062
+ const skipStep = (attrs) => {
6063
+ const { steps } = attrs;
6064
+ const currentStepData = steps[state.currentStep];
6065
+ if (currentStepData && currentStepData.optional) {
6066
+ goToStep(state.currentStep + 1, attrs);
6067
+ }
6068
+ };
6069
+ return {
6070
+ oninit: ({ attrs }) => {
6071
+ state = {
6072
+ id: uniqueId(),
6073
+ currentStep: attrs.currentStep || 0,
6074
+ isValidating: false,
6075
+ completedSteps: new Set(),
6076
+ errorSteps: new Set()
6077
+ };
6078
+ },
6079
+ onbeforeupdate: ({ attrs }) => {
6080
+ // Sync external currentStep changes
6081
+ if (typeof attrs.currentStep === 'number' && attrs.currentStep !== state.currentStep) {
6082
+ state.currentStep = Math.max(0, attrs.currentStep);
6083
+ }
6084
+ },
6085
+ view: ({ attrs }) => {
6086
+ const { steps, showStepNumbers = true, className = '', showNavigation = true, labels = {}, orientation = 'horizontal', allowHeaderNavigation = false } = attrs;
6087
+ // Ensure currentStep is within bounds
6088
+ if (state.currentStep >= steps.length) {
6089
+ state.currentStep = Math.max(0, steps.length - 1);
6090
+ }
6091
+ const currentStepData = steps[state.currentStep];
6092
+ const isFirstStep = state.currentStep === 0;
6093
+ const isLastStep = state.currentStep === steps.length - 1;
6094
+ const activeContent = (currentStepData === null || currentStepData === void 0 ? void 0 : currentStepData.vnode) ? currentStepData.vnode() : null;
6095
+ return m('.wizard', { class: `${orientation} ${className}` }, [
6096
+ // Step indicator
6097
+ m('.wizard-header', [
6098
+ m('.wizard-steps', steps.map((step, index) => {
6099
+ const isActive = index === state.currentStep;
6100
+ const isCompleted = state.completedSteps.has(index);
6101
+ const hasError = state.errorSteps.has(index);
6102
+ return m('.wizard-step', {
6103
+ class: [
6104
+ isActive ? 'active' : '',
6105
+ isCompleted ? 'completed' : '',
6106
+ hasError ? 'error' : '',
6107
+ step.disabled ? 'disabled' : '',
6108
+ step.optional ? 'optional' : ''
6109
+ ].filter(Boolean).join(' '),
6110
+ onclick: allowHeaderNavigation && !step.disabled ?
6111
+ () => goToStep(index, attrs) : undefined
6112
+ }, [
6113
+ // Step number/icon
6114
+ m('.wizard-step-indicator', [
6115
+ isCompleted ?
6116
+ m('i.material-icons', 'check') :
6117
+ hasError ?
6118
+ m('i.material-icons', 'error') :
6119
+ step.icon ?
6120
+ m('i.material-icons', step.icon) :
6121
+ showStepNumbers ?
6122
+ m('span.wizard-step-number', index + 1) :
6123
+ null
6124
+ ]),
6125
+ // Step content
6126
+ m('.wizard-step-content', [
6127
+ m('.wizard-step-title', step.title),
6128
+ step.subtitle && m('.wizard-step-subtitle', step.subtitle),
6129
+ step.optional && m('.wizard-step-optional', labels.optional || 'Optional')
6130
+ ]),
6131
+ // Connector line (except for last step in horizontal mode)
6132
+ orientation === 'horizontal' && index < steps.length - 1 &&
6133
+ m('.wizard-step-connector')
6134
+ ]);
6135
+ }))
6136
+ ]),
6137
+ // Step content
6138
+ m('.wizard-body', [
6139
+ activeContent && m('.wizard-step-panel', {
6140
+ key: (currentStepData === null || currentStepData === void 0 ? void 0 : currentStepData.id) || `step-${state.currentStep}`
6141
+ }, activeContent)
6142
+ ]),
6143
+ // Navigation
6144
+ showNavigation && m('.wizard-footer', [
6145
+ m('.wizard-navigation', [
6146
+ // Previous button
6147
+ !isFirstStep && m('button.btn-flat.wizard-btn-previous', {
6148
+ onclick: () => previousStep(attrs),
6149
+ disabled: state.isValidating
6150
+ }, labels.previous || 'Previous'),
6151
+ // Skip button (for optional steps)
6152
+ currentStepData && currentStepData.optional && !isLastStep &&
6153
+ m('button.btn-flat.wizard-btn-skip', {
6154
+ onclick: () => skipStep(attrs),
6155
+ disabled: state.isValidating
6156
+ }, labels.skip || 'Skip'),
6157
+ // Next/Complete button
6158
+ m('button.btn.wizard-btn-next', {
6159
+ onclick: () => nextStep(attrs),
6160
+ disabled: state.isValidating,
6161
+ class: isLastStep ? 'wizard-btn-complete' : ''
6162
+ }, [
6163
+ state.isValidating && m('i.material-icons.left', 'hourglass_empty'),
6164
+ isLastStep ? (labels.complete || 'Complete') : (labels.next || 'Next')
6165
+ ])
6166
+ ])
6167
+ ])
6168
+ ]);
6169
+ }
6170
+ };
6171
+ };
6172
+ /**
6173
+ * Simple linear stepper for forms
6174
+ */
6175
+ const Stepper = () => {
6176
+ return {
6177
+ view: ({ attrs }) => {
6178
+ return m(Wizard, Object.assign(Object.assign({}, attrs), { linear: true, showNavigation: false, allowHeaderNavigation: false, orientation: 'horizontal' }));
6179
+ }
6180
+ };
6181
+ };
6182
+
6183
+ export { AnchorItem, Autocomplete, Breadcrumb, BreadcrumbManager, Button, ButtonFactory, Carousel, CharacterCounter, Chips, CodeBlock, Collapsible, CollapsibleItem, Collection, CollectionMode, ColorInput, DatePicker, Dropdown, EmailInput, FileInput, FileUpload, FlatButton, FloatingActionButton, HelperText, Icon, InputCheckbox, Label, LargeButton, ListItem, Mandatory, MaterialBox, ModalPanel, NumberInput, Options, Pagination, Parallax, PasswordInput, Pushpin, PushpinComponent, RadioButton, RadioButtons, RangeInput, RoundIconButton, SearchSelect, SecondaryContent, Select, Sidenav, SidenavItem, SidenavManager, SmallButton, Stepper, SubmitButton, Switch, Tabs, TextArea, TextInput, ThemeManager, ThemeSwitcher, ThemeToggle, TimePicker, Toast, ToastComponent, Tooltip, TooltipComponent, UrlInput, Wizard, createBreadcrumb, getDropdownStyles, initPushpins, initTooltips, isNumeric, padLeft, range, toast, uniqueId, uuid4 };