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