@smilodon/core 1.3.13 → 1.4.0

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
@@ -1555,16 +1555,55 @@ class SelectOption extends HTMLElement {
1555
1555
  this._shadow.appendChild(style);
1556
1556
  }
1557
1557
  _render() {
1558
- const { item, index, selected, disabled, active, render, showRemoveButton } = this._config;
1558
+ const { item, index, selected, disabled, active, render, showRemoveButton, classMap } = this._config;
1559
1559
  // Clear container
1560
1560
  this._container.innerHTML = '';
1561
- // Apply state classes
1562
- this._container.classList.toggle('selected', selected);
1563
- this._container.classList.toggle('disabled', disabled || false);
1564
- this._container.classList.toggle('active', active || false);
1561
+ // Add part attribute
1562
+ this._container.setAttribute('part', 'option');
1563
+ // Standard styling hook
1564
+ this._container.classList.add('smilodon-option');
1565
+ // Apply state classes using classMap or defaults
1566
+ const selectedClasses = (classMap?.selected ?? 'selected sm-selected').split(' ').filter(Boolean);
1567
+ const activeClasses = (classMap?.active ?? 'active sm-active').split(' ').filter(Boolean);
1568
+ const disabledClasses = (classMap?.disabled ?? 'disabled sm-disabled').split(' ').filter(Boolean);
1569
+ // Apply classes to both the container (internal styling) and the host (external styling/::part)
1570
+ // This ensures that utility classes are visible via ::part selectors
1571
+ const toggleClasses = (element, classes, add) => {
1572
+ if (add) {
1573
+ element.classList.add(...classes);
1574
+ }
1575
+ else {
1576
+ element.classList.remove(...classes);
1577
+ }
1578
+ };
1579
+ if (selected) {
1580
+ toggleClasses(this._container, [...selectedClasses, 'smilodon-option--selected'], true);
1581
+ toggleClasses(this, [...selectedClasses, 'smilodon-option--selected'], true);
1582
+ }
1583
+ else {
1584
+ toggleClasses(this._container, [...selectedClasses, 'smilodon-option--selected'], false);
1585
+ toggleClasses(this, [...selectedClasses, 'smilodon-option--selected'], false);
1586
+ }
1587
+ if (active) {
1588
+ toggleClasses(this._container, [...activeClasses, 'smilodon-option--active'], true);
1589
+ toggleClasses(this, [...activeClasses, 'smilodon-option--active'], true); // Make focus ring visible on host
1590
+ }
1591
+ else {
1592
+ toggleClasses(this._container, [...activeClasses, 'smilodon-option--active'], false);
1593
+ toggleClasses(this, [...activeClasses, 'smilodon-option--active'], false);
1594
+ }
1595
+ if (disabled) {
1596
+ toggleClasses(this._container, [...disabledClasses, 'smilodon-option--disabled'], true);
1597
+ toggleClasses(this, [...disabledClasses, 'smilodon-option--disabled'], true);
1598
+ }
1599
+ else {
1600
+ toggleClasses(this._container, [...disabledClasses, 'smilodon-option--disabled'], false);
1601
+ toggleClasses(this, [...disabledClasses, 'smilodon-option--disabled'], false);
1602
+ }
1565
1603
  // Custom class name
1566
1604
  if (this._config.className) {
1567
- this._container.className += ' ' + this._config.className;
1605
+ const classes = this._config.className.split(' ').filter(Boolean);
1606
+ this._container.classList.add(...classes);
1568
1607
  }
1569
1608
  // Apply custom styles
1570
1609
  if (this._config.style) {
@@ -1573,12 +1612,13 @@ class SelectOption extends HTMLElement {
1573
1612
  // Render content
1574
1613
  const contentDiv = document.createElement('div');
1575
1614
  contentDiv.className = 'option-content';
1615
+ // contentDiv.setAttribute('part', 'option-content'); // Optional
1576
1616
  if (render) {
1577
1617
  const rendered = render(item, index);
1578
1618
  if (typeof rendered === 'string') {
1579
1619
  contentDiv.innerHTML = rendered;
1580
1620
  }
1581
- else {
1621
+ else if (rendered instanceof HTMLElement) {
1582
1622
  contentDiv.appendChild(rendered);
1583
1623
  }
1584
1624
  }
@@ -1592,16 +1632,56 @@ class SelectOption extends HTMLElement {
1592
1632
  this._removeButton = document.createElement('button');
1593
1633
  this._removeButton.className = 'remove-button';
1594
1634
  this._removeButton.innerHTML = '×';
1635
+ this._removeButton.setAttribute('part', 'chip-remove');
1595
1636
  this._removeButton.setAttribute('aria-label', 'Remove option');
1596
1637
  this._removeButton.setAttribute('type', 'button');
1597
1638
  this._container.appendChild(this._removeButton);
1598
1639
  }
1599
- // Set ARIA attributes
1640
+ // Set ARIA attributes and State attributes on Host
1600
1641
  this.setAttribute('role', 'option');
1601
1642
  this.setAttribute('aria-selected', String(selected));
1602
1643
  if (disabled)
1603
1644
  this.setAttribute('aria-disabled', 'true');
1604
1645
  this.id = this._config.id || `select-option-${index}`;
1646
+ // Add checkmark (part="checkmark") - standard for object mode
1647
+ // Only show if NOT showing remove button (avoid clutter)
1648
+ if (!showRemoveButton) {
1649
+ const checkmark = document.createElement('div');
1650
+ checkmark.setAttribute('part', 'checkmark');
1651
+ checkmark.className = 'checkmark-icon';
1652
+ checkmark.innerHTML = `
1653
+ <svg viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg" style="width:1em;height:1em;">
1654
+ <path d="M4 8.5L6.5 11L12 5.5" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/>
1655
+ </svg>
1656
+ `;
1657
+ // Visibility control via CSS or inline style
1658
+ // We set it to display: none unless selected.
1659
+ // User can override this behavior via part styling if they want transitions
1660
+ if (!selected) {
1661
+ checkmark.style.display = 'none';
1662
+ }
1663
+ else {
1664
+ checkmark.style.marginLeft = '8px';
1665
+ checkmark.style.color = 'currentColor';
1666
+ }
1667
+ this._container.appendChild(checkmark);
1668
+ }
1669
+ // Data Attributes Contract on Host
1670
+ const state = [];
1671
+ if (selected)
1672
+ state.push('selected');
1673
+ if (active)
1674
+ state.push('active');
1675
+ if (state.length) {
1676
+ this.dataset.smState = state.join(' ');
1677
+ }
1678
+ else {
1679
+ delete this.dataset.smState;
1680
+ }
1681
+ this.dataset.smIndex = String(index);
1682
+ if (!this.hasAttribute('data-sm-selectable')) {
1683
+ this.toggleAttribute('data-sm-selectable', true);
1684
+ }
1605
1685
  }
1606
1686
  _attachEventListeners() {
1607
1687
  // Click handler for selection
@@ -1828,10 +1908,12 @@ class EnhancedSelect extends HTMLElement {
1828
1908
  _createInputContainer() {
1829
1909
  const container = document.createElement('div');
1830
1910
  container.className = 'input-container';
1911
+ container.setAttribute('part', 'button');
1831
1912
  return container;
1832
1913
  }
1833
1914
  _createInput() {
1834
1915
  const input = document.createElement('input');
1916
+ input.setAttribute('part', 'input');
1835
1917
  input.type = 'text';
1836
1918
  input.className = 'select-input';
1837
1919
  input.id = `${this._uniqueId}-input`;
@@ -1859,6 +1941,7 @@ class EnhancedSelect extends HTMLElement {
1859
1941
  _createDropdown() {
1860
1942
  const dropdown = document.createElement('div');
1861
1943
  dropdown.className = 'select-dropdown';
1944
+ dropdown.setAttribute('part', 'listbox');
1862
1945
  dropdown.style.display = 'none';
1863
1946
  if (this._config.styles.classNames?.dropdown) {
1864
1947
  dropdown.className += ' ' + this._config.styles.classNames.dropdown;
@@ -1890,7 +1973,7 @@ class EnhancedSelect extends HTMLElement {
1890
1973
  const container = document.createElement('div');
1891
1974
  container.className = 'dropdown-arrow-container';
1892
1975
  container.innerHTML = `
1893
- <svg class="dropdown-arrow" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg">
1976
+ <svg class="dropdown-arrow" part="arrow" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg">
1894
1977
  <path d="M4 6L8 10L12 6" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/>
1895
1978
  </svg>
1896
1979
  `;
@@ -2412,6 +2495,22 @@ class EnhancedSelect extends HTMLElement {
2412
2495
  const query = e.target.value;
2413
2496
  this._handleSearch(query);
2414
2497
  });
2498
+ // Delegated click listener for improved event handling (smart fallback)
2499
+ this._optionsContainer.addEventListener('click', (e) => {
2500
+ const target = e.target;
2501
+ // Handle option clicks
2502
+ const option = target.closest('[data-sm-selectable], [data-selectable], [data-sm-state]');
2503
+ if (option && !option.hasAttribute('aria-disabled')) {
2504
+ const indexStr = option.getAttribute('data-sm-index') ?? option.getAttribute('data-index');
2505
+ const index = Number(indexStr);
2506
+ if (!Number.isNaN(index)) {
2507
+ this._selectOption(index, {
2508
+ shiftKey: e.shiftKey,
2509
+ toggleKey: e.ctrlKey || e.metaKey,
2510
+ });
2511
+ }
2512
+ }
2513
+ });
2415
2514
  // Keyboard navigation
2416
2515
  this._input.addEventListener('keydown', (e) => this._handleKeydown(e));
2417
2516
  // Click outside to close
@@ -2984,10 +3083,12 @@ class EnhancedSelect extends HTMLElement {
2984
3083
  selectedEntries.forEach(([index, item]) => {
2985
3084
  const badge = document.createElement('span');
2986
3085
  badge.className = 'selection-badge';
3086
+ badge.setAttribute('part', 'chip');
2987
3087
  badge.textContent = getLabel(item);
2988
3088
  // Add remove button to badge
2989
3089
  const removeBtn = document.createElement('button');
2990
3090
  removeBtn.className = 'badge-remove';
3091
+ removeBtn.setAttribute('part', 'chip-remove');
2991
3092
  removeBtn.innerHTML = '×';
2992
3093
  removeBtn.setAttribute('aria-label', `Remove ${getLabel(item)}`);
2993
3094
  removeBtn.addEventListener('click', (e) => {
@@ -3355,6 +3456,7 @@ class EnhancedSelect extends HTMLElement {
3355
3456
  if (this._state.isSearching) {
3356
3457
  const searching = document.createElement('div');
3357
3458
  searching.className = 'searching-state';
3459
+ searching.setAttribute('part', 'loading');
3358
3460
  searching.textContent = 'Searching...';
3359
3461
  this._optionsContainer.appendChild(searching);
3360
3462
  return;
@@ -3412,6 +3514,7 @@ class EnhancedSelect extends HTMLElement {
3412
3514
  });
3413
3515
  if (!hasRenderedItems && !this._state.isBusy) {
3414
3516
  const empty = document.createElement('div');
3517
+ empty.setAttribute('part', 'no-results');
3415
3518
  empty.className = 'empty-state';
3416
3519
  if (query) {
3417
3520
  empty.textContent = `No results found for "${this._state.searchQuery}"`;
@@ -3425,6 +3528,7 @@ class EnhancedSelect extends HTMLElement {
3425
3528
  // Append Busy Indicator if busy
3426
3529
  if (this._state.isBusy && this._config.busyBucket.enabled) {
3427
3530
  const busyBucket = document.createElement('div');
3531
+ busyBucket.setAttribute('part', 'loading');
3428
3532
  busyBucket.className = 'busy-bucket';
3429
3533
  if (this._config.busyBucket.showSpinner) {
3430
3534
  const spinner = document.createElement('div');
@@ -3473,11 +3577,24 @@ class EnhancedSelect extends HTMLElement {
3473
3577
  getValue,
3474
3578
  getLabel,
3475
3579
  showRemoveButton: this._config.selection.mode === 'multi' && this._config.selection.showRemoveButton,
3580
+ classMap: this.classMap,
3476
3581
  });
3582
+ // Valid part attribute on the web component host itself
3583
+ option.setAttribute('part', 'option');
3477
3584
  option.dataset.index = String(index);
3478
3585
  option.dataset.value = String(getValue(item));
3586
+ // New standard attributes on Host
3587
+ option.dataset.smIndex = String(index);
3588
+ if (!option.hasAttribute('data-sm-selectable')) {
3589
+ option.toggleAttribute('data-sm-selectable', true);
3590
+ }
3591
+ const val = getValue(item);
3592
+ if (val != null) {
3593
+ option.dataset.smValue = String(val);
3594
+ }
3479
3595
  option.id = option.id || optionId;
3480
3596
  option.addEventListener('click', (e) => {
3597
+ e.stopPropagation(); // Prevent duplicate handling by delegation
3481
3598
  const mouseEvent = e;
3482
3599
  this._selectOption(index, {
3483
3600
  shiftKey: mouseEvent.shiftKey,
@@ -3493,35 +3610,71 @@ class EnhancedSelect extends HTMLElement {
3493
3610
  }
3494
3611
  _normalizeCustomOptionElement(element, meta) {
3495
3612
  const optionEl = element instanceof HTMLElement ? element : document.createElement('div');
3613
+ // Add part attribute for styling
3614
+ if (!optionEl.hasAttribute('part')) {
3615
+ optionEl.setAttribute('part', 'option');
3616
+ }
3496
3617
  // Add both semantic namespaced classes and the legacy internal classes that CSS uses
3497
3618
  optionEl.classList.add('smilodon-option', 'option');
3498
- // Toggle state classes
3619
+ // Toggle state classes using classMap if available
3499
3620
  const isSelected = meta.selected;
3500
3621
  const isActive = meta.active;
3501
3622
  const isDisabled = meta.disabled;
3623
+ // Resolve classes from classMap or defaults
3624
+ const selectedClasses = (this.classMap?.selected ?? 'selected sm-selected').split(' ').filter(Boolean);
3625
+ const activeClasses = (this.classMap?.active ?? 'active sm-active').split(' ').filter(Boolean);
3626
+ const disabledClasses = (this.classMap?.disabled ?? 'disabled sm-disabled').split(' ').filter(Boolean);
3502
3627
  if (isSelected) {
3503
- optionEl.classList.add('smilodon-option--selected', 'selected');
3628
+ optionEl.classList.add(...selectedClasses);
3629
+ optionEl.classList.add('smilodon-option--selected');
3504
3630
  }
3505
3631
  else {
3506
- optionEl.classList.remove('smilodon-option--selected', 'selected');
3632
+ optionEl.classList.remove(...selectedClasses);
3633
+ optionEl.classList.remove('smilodon-option--selected');
3507
3634
  }
3508
3635
  if (isActive) {
3509
- optionEl.classList.add('smilodon-option--active', 'active');
3636
+ optionEl.classList.add(...activeClasses);
3637
+ optionEl.classList.add('smilodon-option--active');
3510
3638
  }
3511
3639
  else {
3512
- optionEl.classList.remove('smilodon-option--active', 'active');
3640
+ optionEl.classList.remove(...activeClasses);
3641
+ optionEl.classList.remove('smilodon-option--active');
3513
3642
  }
3514
3643
  if (isDisabled) {
3515
- optionEl.classList.add('smilodon-option--disabled', 'disabled');
3644
+ optionEl.classList.add(...disabledClasses);
3645
+ optionEl.classList.add('smilodon-option--disabled');
3516
3646
  }
3517
3647
  else {
3518
- optionEl.classList.remove('smilodon-option--disabled', 'disabled');
3648
+ optionEl.classList.remove(...disabledClasses);
3649
+ optionEl.classList.remove('smilodon-option--disabled');
3650
+ }
3651
+ // Data Attributes Contract
3652
+ const state = [];
3653
+ if (isSelected)
3654
+ state.push('selected');
3655
+ if (isActive)
3656
+ state.push('active');
3657
+ if (state.length) {
3658
+ optionEl.dataset.smState = state.join(' ');
3519
3659
  }
3660
+ else {
3661
+ delete optionEl.dataset.smState;
3662
+ }
3663
+ // Legacy data attribute support
3520
3664
  if (!optionEl.hasAttribute('data-selectable')) {
3521
3665
  optionEl.setAttribute('data-selectable', '');
3522
3666
  }
3667
+ // New delegation attribute
3668
+ if (!optionEl.hasAttribute('data-sm-selectable')) {
3669
+ optionEl.setAttribute('data-sm-selectable', '');
3670
+ }
3523
3671
  optionEl.dataset.index = String(meta.index);
3524
3672
  optionEl.dataset.value = String(meta.value);
3673
+ // New standard attributes
3674
+ optionEl.dataset.smIndex = String(meta.index);
3675
+ if (meta.value != null) {
3676
+ optionEl.dataset.smValue = String(meta.value);
3677
+ }
3525
3678
  optionEl.id = optionEl.id || meta.id;
3526
3679
  if (!optionEl.getAttribute('role')) {
3527
3680
  optionEl.setAttribute('role', 'option');
@@ -3541,6 +3694,7 @@ class EnhancedSelect extends HTMLElement {
3541
3694
  }
3542
3695
  if (!meta.disabled) {
3543
3696
  optionEl.addEventListener('click', (e) => {
3697
+ e.stopPropagation(); // Prevent duplicate handling by delegation
3544
3698
  const mouseEvent = e;
3545
3699
  this._selectOption(meta.index, {
3546
3700
  shiftKey: mouseEvent.shiftKey,