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

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