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