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