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

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